SteamVR(HTC Vive) Unity插件深度分析(四)
4. Editor
SteamVR/Editor/目录下的几个脚本都是用于定制SteamVR插件中部分脚本在Unity中的Inspector界面显示及功能的。
4.1. SteamVR_Editor.cs
这个脚本的作用是定制SteamVR_Camera组件的Inspector显示(SteamVR_Camera是一个脚本,当把它以组件添加到某个对象上时,就能在这个对象的Inspector里面看到了。如果没有定制界面显示,通常情况下一个脚本组件只会显示相关的public的并且Unity能够识别并显示的变量)。关于如何定制Editor可以参看官方文档:http://docs.unity3d.com/Manual/editor-CustomEditors.html
因为SteamVR_Camera.cs会添加到原始的主相机上,因此我们可以在原始主相机的Inspector视图中看到SteamVR_Editor.cs的效果。以SteamVR Unity插件中的example为例,它位于Main Camera(origin)->MainCamera(head)->Main Camera(eye)上面:
注:后面对于SteamVR插件中所有脚本均会逐行分析
[CustomEditor(typeof(SteamVR_Camera)), CanEditMultipleObjects]
CustomEditor是Unity定义的一个属性,它的作用是告诉Unity这个脚本的角色是什么。像这里它是typeof(SteamVR_Camera),表示它是SteamVR_Camera组件的Editor。后面的CanEditMulipleObjects表示选中多个同类型对象,可以通过这个Editor(Inspector)批量修改它们的属性
public class SteamVR_Editor : Editor
{
这里的banner就是在界面上看到的一个SteamVR的图标。如图:
这里指定了高度
int bannerHeight = 150;
logo图像对象,这里使用了UnityEngine里面的Texture类。
Texture logo;
SerializedProperty用于描述Insperctor中的属性,它与SerializedObject一起使用, SerializedProperty表示的属性项的具体特性在Unity文档中并找不到,但从名字看显然 它是可以序列化的,也就是可以持久保存和加载的,Unity的文档中还提到它可以自动 处理undo、多对象编辑、预制体更新等功能
SerializedProperty script, wireframe;
获取logo所在的目录
string GetResourcePath()
{
var ms = MonoScript.FromScriptableObject(this);
var path = AssetDatabase.GetAssetPath(ms);
path = Path.GetDirectoryName(path);
路径在SteamVR/Textures/目录
return path.Substring(0,path.Length - "Editor".Length) + "Textures/";
}
OnEnable是Editor的父类ScriptableObject中的方法。OnEnable是在对象 (这里就是 SteamVR_Editor)加载的时候被调用(这里应该就是需要展示SteamVR_Camera脚本的 属性时)。
void OnEnable()
{
var resourcePath = GetResourcePath();
#if UNITY_5_0
5.0使用Resources来加载资源
logo= Resources.LoadAssetAtPath
其它版本使用AssetDatabase来加载资源
logo= AssetDatabase.LoadAssetAtPath<Texture2D>(resourcePath+ "logo.png");
#endif
serializedObject通过MonoDevelop的反编译可以知道是Editor类中的一个 SerializedObject对象。下面两个SerializedProperty有点不知道怎么来的, m_Script应该是Unity自己的一个属性,它用于标识脚本组件对应的脚本名称, 这也可以看到在正常的脚本组件中也有一个Script属性,不过是不可更改的。 在 SteamVR_Camera脚本组件中,Script是可以改的,是因为这个控件是在下面的代 码中自己创建的,加个只读属性就好了。而wireframe这个属性,就是 SteamVR_Camera.cs中导出 来的(public的成员变量)。在正常的脚本组件中,Script 属性和其它脚本中public出来的属性都是会自动显示在Inspector中的。这里因为 是自定义的,所以要自己显示出来。TODO 找一下系统是在哪实现的。感觉 serializedObject之所以可以找到这些属性,应该是在解析脚本文件时检测到了这些 变量,然后把它加到serializedObject当中了——这个是找不到的,Unity这部分并 不开源。
script =serializedObject.FindProperty("m_Script"); wireframe= serializedObject.FindProperty("wireframe");
targets是Editor中的一个变量,里面还有一个target变量。target的意思应该是 当前Editor对应的组件所在的目标对象,比如,当前的SteamVR_Editor对应的组 件是SteamVR_Camera脚本,而脚本必须依附在一个对象上,比如在 SteamVR的 插件中,SteamVR_Camera要添加到场景的主相机中,那么这里的主相机就是target。 而targets应该是批量选中多个带SteamVR_Camera脚本时的情况。下面就是将 SteamVR_Camera脚本放到所有的目标对象的最后一个组件。ForcaeLast是 SteamVR_Camera类中的一个方法,用于将自己放到所在目标的最后一个组件
foreach (SteamVR_Camera target in targets) target.ForceLast();
}
OnInspectorGUI是真正绘制Inspector界面的地方。如果调用父类的 DrawdefaultInspector就会是缺省的界面。而这里要定制界面显示
public override void OnInspectorGUI()
{
这个应该是更新将编辑器中改变的内容更新到代码中的属性中。这个调用基本上都 是套路,通常在OnInspectorGUI调用的第一个方法就是它。它的作用类似于MFC 对话框中的UpdateData(TRUE)
serializedObject.Update();
获取logo绘制的位置,宽度位置上留了38像素的空。第三个参数为指定 矩形的 类型,会使用它的margin和padding参数
var rect = GUILayoutUtility.GetRect(Screen.width- 38, bannerHeight, GUI.skin.box);
if (logo)
画logo
GUI.DrawTexture(rect,logo, ScaleMode.ScaleToFit);
if (!Application.isPlaying)
{
在没有播放的时候才画那个Expand/Collapse按钮
var expand = false;
var collapse = false;
foreach (SteamVR_Camera target in targets)
{
if (AssetDatabase.Contains(target))
这里是判断SteamVR_Camera脚本所在的对象(物体)是否 是一个 asset(本地asset文件),如果是的,则忽略。因为SteamVR_Camera 通常是加到主相机上,而主相机不是一个asset。加到asset中的情况 应该就是预制体的情况
continue;
if (target.isExpanded)
collapse= true;
else
expand= true;
}
上面虽然对targets进行了遍历,但最终起作用的只是最后一个target,所以没 什么用。但通常情况下,一个场景中只会有一个SteamVR_Camera
if (expand)
{
显示Expand按钮
GUILayout.BeginHorizontal();
if (GUILayout.Button("Expand"))
{
这里表示用户点击了Expand按钮。这里太奇怪了。代码在这里又不 会阻塞,这是C#语言的特性?与通常理解的代码的自然流向有冲突
foreach (SteamVR_Camera target in targets)
{
if (AssetDatabase.Contains(target))
continue;
if (!target.isExpanded)
{
如果target还没有Expand,则调用Expand方法。Expand 的作用就是将一个普通的Camera变成SteamVR的Camera, 比如增加头部、眼睛、耳朵等。详情见 SteamVR_Camera.Expand的分析
target.Expand();
将target标记为“脏”。“脏”的意思就是数据已经修改
EditorUtility.SetDirty(target);
}
}
}
GUILayout.Space(18);
GUILayout.EndHorizontal();
}
if (collapse)
{
显示Collapse按钮
GUILayout.BeginHorizontal();
if (GUILayout.Button("Collapse"))
{
用户点击了Collapse按钮
foreach (SteamVR_Camera target in targets)
{
if (AssetDatabase.Contains(target))
continue;
if (target.isExpanded)
{
target.Collapse();
EditorUtility.SetDirty(target);
}
}
}
GUILayout.Space(18);
GUILayout.EndHorizontal();
}
}
将script和wireframe属性显示出来,即显示如图的两个控件:
EditorGUILayout.PropertyField(script);
EditorGUILayout.PropertyField(wireframe);
这里所谓的应用修改的属性,是将Inspector中的改变应用到场景中。这也是套路, 在OnInspectorGUI的开始调用serializedObject.Update,在结尾调用 serializedObject.ApplyModifiedProperties。类似于Windows MFC中的 UpdateData(TRUE)和UpdateData(FALSE),分别是将界面应用到数据和将数据应用 到界面
serializedObject.ApplyModifiedProperties();
}
这个方法应该是供外部调用导出unitypackage的(也就是生成SteamVR插件包的),搜 索了代码,其实没有地方调用,显然是SteamVR官方开发者用的。而且它最后有一句 EditorApplication.Exit,会退出整个Unity
public static void ExportPackage()
{
AssetDatabase.ExportPackage(new string[] {
"Assets/SteamVR",
"Assets/Plugins/openvr_api.cs",
"Assets/Plugins/openvr_api.bundle",
"Assets/Plugins/x86/openvr_api.dll",
"Assets/Plugins/x86/steam_api.dll",
"Assets/Plugins/x86/libsteam_api.so",
"Assets/Plugins/x86_64/openvr_api.dll",
"Assets/Plugins/x86_64/steam_api.dll",
"Assets/Plugins/x86_64/libsteam_api.so",
"Assets/Plugins/x86_64/libopenvr_api.so",
} , "steamvr.unitypackage", ExportPackageOptions.Recurse);
EditorApplication.Exit(0);
}
}
4.2. SteamVR_RenderModelEditor.cs
这个是针对SteamVR_RenderModel.cs脚本定制Inspector的Editor脚本。什么是RenderModel呢?翻译过来渲染模型。它是针对SteamVR的一些跟踪设备提供渲染模型的,比如手柄,头显、基站等,也就是为这些设备在场景中显示出来提供渲染模型(网格、纹理什么的)。这些模型在
SteamVR_RenderModel.cs可以加到需要显示的跟踪对象上,通常就是手柄上。下图为官方示例example.unity中左控制器(即左手柄)下Model子物体上的情况:
当然左控制器(Controller(left))本身是一个跟踪对象(Tracked Device):
右控制器下面的RenderModel:
头显上并没有RenderModel,只有TrackedObject,想想用户自己并看不到自己,所以头显的RenderModel并没有用:
其它跟踪对象都位于Hierarchy下面的Tracked Devices下面,总共15个,加上头显(头显总是第一个跟踪设备),总共16个。16个是SteamVR最多支持的跟踪设备的数量。随便找一个device看一下:
仍然逐行分析代码:
下面这行表示它是SteamVR_RenderModel这个组件的属性编辑器,并且支持多对象选中编辑
[CustomEditor(typeof(SteamVR_RenderModel)), CanEditMultipleObjects]
public class SteamVR_RenderModelEditor : Editor
{
属性很多,第一个script总是有的,表示当前Editor是针对哪个脚本的。后面的几个属 性都是SteamVR_RenderModel中的public出来的变量。这些属性都会在Inspector中显 示出来
SerializedProperty script, index, modelOverride, shader, verbose, createComponents,updateDynamically;
这个是所有的渲染模型的名字。大概就是SteamVRresource目录下的rendermodels目录 下的那些子目录——经确认,确实就是rendermodels下面的子目录。下图是下面的 modelOverride列表中的内容(显示的就是renderModelNames中的内容):
正好就是rendermodels目录下的全部子目录:
static string[] renderModelNames;
这个是渲染模型的索引。就是上面名字数组中的索引
int renderModelIndex;
void OnEnable()
{
获取SteamVR_RenderModel中public的属性,这些属性的意义参看后面的分析
script= serializedObject.FindProperty("m_Script");
index =serializedObject.FindProperty("index");
modelOverride =serializedObject.FindProperty("modelOverride");
shader =serializedObject.FindProperty("shader");
verbose =serializedObject.FindProperty("verbose");
createComponents =serializedObject.FindProperty("createComponents");
updateDynamically= serializedObject.FindProperty("updateDynamically");
//Load render model names if necessary.
if (renderModelNames == null)
{
加载所有的渲染模型的名字
renderModelNames= LoadRenderModelNames();
}
//Update renderModelIndex based on current modelOverride value.
从SteamVR_RenderModel中取出的modelOverride属性可能已经有值了。根据 这个值定位它在renderModelNames中的索引
if (modelOverride.stringValue != "")
{
for (int i = 0;i < renderModelNames.Length; i++)
{
if (modelOverride.stringValue == renderModelNames[i])
{
renderModelIndex= i;
break;
}
}
}
}
加载所有的渲染模型名字。这里的名字列表就是resources/rendermodels目录下的子目录
static string[] LoadRenderModelNames()
{
var results = new List<string>();
添加第一个缺省的名字为None
results.Add("None");
RenderModelInterfaceHolder是SteamVR_RenderModel中的内部类,里面封装了对 IVRRenderModel接口的获取。这里使用了using语法糖,它的作用是在代码块结 束的时候会自动调用holder对象的Dispose方法,以达到类似C++的在代码块结束 时自动析构的效果
using (var holder = new SteamVR_RenderModel.RenderModelInterfaceHolder())
{
Holder.instance中保存的是CVRRenderModels对象
var renderModels = holder.instance;
if (renderModels != null)
{
获取模型数量
uint count = renderModels.GetRenderModelCount();
for (uint i = 0;i < count; i++)
{
var buffer = new StringBuilder();
获取指定索引的模型名字。做法是先传入0字节的缓冲区以获取所需 要的缓冲区大小,然后再分配空间再次调用
var requiredSize = renderModels.GetRenderModelName(i, buffer, 0);
if (requiredSize == 0)
continue;
buffer.EnsureCapacity((int)requiredSize);
renderModels.GetRenderModelName(i,buffer, requiredSize);
results.Add(buffer.ToString());
}
}
}
return results.ToArray();
}
绘制UI
public override void OnInspectorGUI()
{
从编辑器(SteamVR_RenderModel变量中)获取/更新属性值
serializedObject.Update();
使用EditorGUILayout.PropertyField能自动根据属性类型生成界面
EditorGUILayout.PropertyField(script);
EditorGUILayout.PropertyField(index);
//EditorGUILayout.PropertyField(modelOverride);
modelOverride本身是一个字符串,但由于渲染模型有多个,实际上相当于一个字 符串枚举,在界面上需要有一个下拉列表来选择。因此不能使用 EditorGUILayout.PropertyField,而要自己画出来
GUILayout.BeginHorizontal();
GUILayout.Label("ModelOverride");
估计这个Popup也是会等用户选择才能返回。参数为初始选中的索引和列表
var selected = EditorGUILayout.Popup(renderModelIndex,renderModelNames);
if (selected != renderModelIndex)
{
renderModelIndex= selected;
modelOverride.stringValue= (selected > 0) ?renderModelNames[selected] : "";
}
GUILayout.EndHorizontal();
EditorGUILayout.PropertyField(shader);
EditorGUILayout.PropertyField(verbose);
EditorGUILayout.PropertyField(createComponents);
EditorGUILayout.PropertyField(updateDynamically);
将通过Inspector视图修改的值反向更新到SteamVR_RenderModel类的变量中及场 景中
serializedObject.ApplyModifiedProperties();
}
}
4.3. SteamVR_Settings.cs
这个是在导入SteamVR Unity插件的时候弹出的设置对话框:
它是用于全局的编译参数设置的,就是自动设置(部分)编译参数,即PlaySettings,如下图(File菜单下的Build Settings):
InitializeOnLoad是Unity定义的一个属性,定义了这个属性的类会在Unity启动的时候调用。因为上面这个对话框在导入unitypackage之后也会弹出来,感觉在import之后也会立即调用。
[InitializeOnLoad]
从EditorWindow派生,EditorWindow就是Unity里面的窗口类
public class SteamVR_Settings : EditorWindow
{
强制显示窗口。下面有一系列的判断条件,判断开发者没有做出选择并且缺省配置与推 荐配置不一致时才显示设备窗口。如果forceShow为true,则总是会显示这个窗口。
const bool forceShow = false; // Set to true to get the dialog to show back up in the case youclicked Ignore All.
界面上显示的一些文字、关键字、缺省取值等
const string ignore = "ignore.";
const string useRecommended = "Use recommended ({0})";
const string currentValue = " (current = {0})";
const string buildTarget = "Build Target";
const string showUnitySplashScreen = "Show UnitySplashscreen";
const string defaultIsFullScreen = "Default isFullscreen";
const string defaultScreenSize = "Default Screen Size";
const string runInBackground = "Run In Background";
const string displayResolutionDialog = "Display ResolutionDialog";
const string resizableWindow = "Resizable Window";
const string fullscreenMode = "D3D11 Fullscreen Mode";
const string visibleInBackground = "Visible InBackground";
const string renderingPath = "Rendering Path";
const string colorSpace = "Color Space";
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 ||UNITY_5_0)
5.0以下版本有gpuSkinnnig参数
const string gpuSkinning ="GPU Skinning";
#endif
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0) && false // skyboxes are currently broken
const stringsinglePassStereoRendering = "Single-Pass Stereo Rendering";
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
5.0以上版本的stereoscopicRendering参数
const string stereoscopicRendering = "StereoscopicRendering";
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1)
5.1以上版本才支持VR(是指Unity自身对VR的支持)。从这里可以看出,SteamVR 的Unity插件最多支持的就是Unity 5.3版本了。如果在更新的版本上运行,最好修改 这一部分代码
const string virtualRealitySupported = "VirtualReality Support";
#endif
下面是一些参数的推荐值
编译目标,推荐是在64位windows,因为这个已经是主流了
const BuildTarget recommended_BuildTarget = BuildTarget.StandaloneWindows64;
缺省编译出来的exe启动时会有一个Unity的闪屏,这里推荐是关掉
const bool recommended_ShowUnitySplashScreen = false;
推荐不全屏(这个是否全屏应该是指在PC端的伴随窗口)
const bool recommended_DefaultIsFullScreen = false;
推荐屏幕宽度1024(这里应该也是指伴随窗口的分辨率)
const int recommended_DefaultScreenWidth = 1024;
推荐屏幕高度768
const int recommended_DefaultScreenHeight = 768;
推荐在后台跑,是指窗口即使失去焦点仍然继续运行,对于VR,当然要这样
这个在后面的SteamVR_Render脚本中还会直接设置为true
const bool recommended_RunInBackground = true;
推荐不弹分辨率设置对话框。缺省是在运行前会弹一个对话框进行一些设置的
const ResolutionDialogSetting recommended_DisplayResolutionDialog = ResolutionDialogSetting.HiddenByDefault;
窗口大小可以改变
const bool recommended_ResizableWindow = true;
D3D11FullscreenMode设为全屏模式。另外一种模式是独占模式
const D3D11FullscreenMode recommended_FullscreenMode = D3D11FullscreenMode.FullscreenWindow;
与上面的FullscreenWindow一起使用。为true表示即使切换到其它窗口,它也不会最 小化,而是在其它窗口的后面可见。如果为false则是切换到其它窗口后,当前窗口会 最小化
const bool recommended_VisibleInBackground = true;
所谓的渲染路径,有Deferred Lighting、Forward Rendering、Vertex Lit。关于Unity的 几种渲染路径的区别,参考相关文档
const RenderingPath recommended_RenderPath = RenderingPath.Forward;
颜色空间,有Linear、Gamma。参考相关文档
const ColorSpace recommended_ColorSpace = ColorSpace.Linear;
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 ||UNITY_5_0)
GPU网格蒙皮功能,5.0之前的版本
const boolrecommended_GpuSkinning = true;
#endif
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0) && false
const boolrecommended_SinglePassStereoRendering = true;
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
Direct3D11.1的立体视觉渲染特性
const bool recommended_StereoscopicRendering = false;
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1)
竟然推荐把VR支持关掉?想想大概是因为要使用插件的VR支持而不使用Unity自带 的VR支持
const bool recommended_VirtualRealitySupported = false;
#endif
static SteamVR_Settings window;
静态构造方法
static SteamVR_Settings()
{
EditorApplication.update是一个委托链(作用是回调),多个EditorWindow可以把 自己的需要更新的代码加到这个委托链中。从名字可以看出 EditorApplication.update的作用应该是定期调用的。查阅相关文档可以得知, EditorWindow类其实有一个非static的void Update()方法,也是定期调用的(每秒 会调用100次)。所以不知道这两种有何区别,还是根本就是一样的,只是在不同 的版本引入的。这里的static void Update其实不能构成对void Update的Override, 为避免引起混淆,把static void Update改个其它名字会比较好
EditorApplication.update+= Update;
}
static void Update()
{
是否显示窗口的条件:是否已经保存了用户选择的结果+缺省的配置是否与推荐的 配置一致+forceShow是否设为true。缺省配置与推荐配置项中的任意一项不一致就 会显示
bool show =
(!EditorPrefs.HasKey(ignore+ buildTarget) &&
EditorUserBuildSettings.activeBuildTarget!= recommended_BuildTarget) ||
(!EditorPrefs.HasKey(ignore+ showUnitySplashScreen) &&
PlayerSettings.showUnitySplashScreen!= recommended_ShowUnitySplashScreen) ||
(!EditorPrefs.HasKey(ignore+ defaultIsFullScreen) &&
PlayerSettings.defaultIsFullScreen!= recommended_DefaultIsFullScreen) ||
(!EditorPrefs.HasKey(ignore+ defaultScreenSize) &&
(PlayerSettings.defaultScreenWidth!= recommended_DefaultScreenWidth ||
PlayerSettings.defaultScreenHeight!= recommended_DefaultScreenHeight)) ||
(!EditorPrefs.HasKey(ignore+ runInBackground) &&
PlayerSettings.runInBackground!= recommended_RunInBackground) ||
(!EditorPrefs.HasKey(ignore+ displayResolutionDialog) &&
PlayerSettings.displayResolutionDialog!= recommended_DisplayResolutionDialog) ||
(!EditorPrefs.HasKey(ignore+ resizableWindow) &&
PlayerSettings.resizableWindow!= recommended_ResizableWindow) ||
(!EditorPrefs.HasKey(ignore+ fullscreenMode) &&
PlayerSettings.d3d11FullscreenMode!= recommended_FullscreenMode) ||
(!EditorPrefs.HasKey(ignore+ visibleInBackground) &&
PlayerSettings.visibleInBackground!= recommended_VisibleInBackground) ||
(!EditorPrefs.HasKey(ignore+ renderingPath) &&
PlayerSettings.renderingPath!= recommended_RenderPath) ||
(!EditorPrefs.HasKey(ignore+ colorSpace) &&
PlayerSettings.colorSpace!= recommended_ColorSpace) ||
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 ||UNITY_5_0)
(!EditorPrefs.HasKey(ignore+ gpuSkinning) &&
PlayerSettings.gpuSkinning!= recommended_GpuSkinning) ||
#endif
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0) && false
(!EditorPrefs.HasKey(ignore+ singlePassStereoRendering) &&
PlayerSettings.singlePassStereoRendering!= recommended_SinglePassStereoRendering) ||
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
(!EditorPrefs.HasKey(ignore+ stereoscopicRendering) &&
PlayerSettings.stereoscopic3D!= recommended_StereoscopicRendering) ||
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1)
(!EditorPrefs.HasKey(ignore+ virtualRealitySupported) &&
PlayerSettings.virtualRealitySupported!= recommended_VirtualRealitySupported) ||
#endif
forceShow;
if (show)
{
说是只要调用GetWindow,窗口就会显示出来。我想说这样的API也太奇怪 了
window= GetWindow<SteamVR_Settings>(true);
window.minSize= new Vector2(320, 440);
//window.title= "SteamVR";
}
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 ||UNITY_5_0)
5.0以下版本,切换回Unity自带的VR支持
//Switch to native OpenVR support.
var updated =false;
if(!PlayerSettings.virtualRealitySupported)
{
将virtualRealitySupported设为true
PlayerSettings.virtualRealitySupported= true;
updated= true;
}
遍历Unity原生支持的VR设备,看是否有OpenVR
vardevices =UnityEditorInternal.VR.VREditor.GetVREnabledDevices(BuildTargetGroup.Standalone);
var hasOpenVR =false;
foreach (vardevice in devices)
if(device.ToLower() == "openvr")
hasOpenVR= true;
这之后可以加个break跳出循环
if (!hasOpenVR)
{
如果没有OpenVR,添加OpenVR到支持的设备列表中(TODO 这样就算支 持了?)
string[]newDevices;
if(updated)
{
如果原来就没有打开VR支持,说明没有支持的VR设备,则只添加 OpenVR自身就可以了
newDevices= new string[] { "OpenVR" };
}
else
{
如果原来VR支持就打开了,说明已经有其它VR设备支持了(比如 Oculus),则把OpenVR中到最后面
newDevices= new string[devices.Length + 1];
for(int i = 0; i < devices.Length; i++)
newDevices[i]= devices[i];
newDevices[devices.Length]= "OpenVR";
updated= true;
}
将支持的设备列表更新 UnityEditorInternal.VR.VREditor.SetVREnabledDevices(BuildTargetGroup.Standalone,newDevices);
}
if (updated)
Debug.Log("Switchingto native OpenVR support.");
var dlls = newstring[]
{
"Plugins/x86/openvr_api.dll",
"Plugins/x86_64/openvr_api.dll"
} ;
下面是删掉Plugins目录下的openvr_api.dll(因为要使用Unity自带的VR支持, 就不能使用其它的版本)
foreach(var path in dlls)
{
if(!File.Exists(Application.dataPath + "/" + path))
continue;
if(AssetDatabase.DeleteAsset("Assets/" + path))
Debug.Log("Deleting" + path);
else
{
Debug.Log(path+ " in use; cannot delete. Please restart Unity to completeupgrade.");
}
}
#endif
这里是从委托链中去掉当前的Update回调。前面我还想说Update会一直调用怎么 办,这里看就只会调用一次了。
EditorApplication.update-= Update;
}
Vector2 scrollPosition;
bool toggleState;
获取xxx/Assets/SteamVR/Textures这个目录的全路径
string GetResourcePath()
{
var ms = MonoScript.FromScriptableObject(this);
var path = AssetDatabase.GetAssetPath(ms);
path = Path.GetDirectoryName(path);
return path.Substring(0, path.Length - "Editor".Length)+ "Textures/";
}
OnGUI是绘制界面的地方
public void OnGUI()
{
仍然是在最上面绘制一个SteamVR的logo
var resourcePath = GetResourcePath();
#if !(UNITY_5_0)
var logo = AssetDatabase.LoadAssetAtPath<Texture2D>(resourcePath+ "logo.png");
#else
varlogo = Resources.LoadAssetAtPath
position是父类EditorWindow中的变量,是窗口在屏幕坐标中的位置
var rect = GUILayoutUtility.GetRect(position.width, 150, GUI.skin.box);
if (logo)
GUI.DrawTexture(rect,logo, ScaleMode.ScaleToFit);
HelpBox看起来是这样的控件:
EditorGUILayout.HelpBox("Recommendedproject settings for SteamVR:", MessageType.Warning);
接下来是一个ScrollView,可以看到Unity是在代码中来定义布局,也不知道算好 还是不好
scrollPosition= GUILayout.BeginScrollView(scrollPosition);
int numItems = 0;
下面是将所有的没有保存的选项并且推荐的配置与缺省的配置不一致的选项显示 出来供用户来选择是否选择推荐选项。
if (!EditorPrefs.HasKey(ignore + buildTarget)&&
EditorUserBuildSettings.activeBuildTarget!= recommended_BuildTarget)
{
++numItems;
Label就是显示一个文本框,相当于Windows上的Static,Android中的TextView
GUILayout.Label(buildTarget+ string.Format(currentValue, EditorUserBuildSettings.activeBuildTarget));
开始一个水平布局,类似于Android里面的horizontal的LinearLayout。缺省 是垂直布局
GUILayout.BeginHorizontal();
这个按钮是用来让用户选择推荐的配置的
if (GUILayout.Button(string.Format(useRecommended,recommended_BuildTarget)))
{
这里表示用户点击了这个按钮 EditorUserBuildSettings.SwitchActiveBuildTarget(recommended_BuildTarget);
}
FlexibleSpace看起来作用就是指前后的控件的大小是固定的,而两者之间的空 间是动态变化的
GUILayout.FlexibleSpace();
加一个Ignore按钮,让用户忽略这个选项,意思就是不使用推荐选项而使用 缺省(或者说当前)配置
if (GUILayout.Button("Ignore"))
{
看起来是将忽略的配置项写到了EditorPrefs。EditorPrefs就类似于Android 中的SharedPreferences了
EditorPrefs.SetBool(ignore+ buildTarget, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore +showUnitySplashScreen) &&
PlayerSettings.showUnitySplashScreen!= recommended_ShowUnitySplashScreen)
{
++numItems;
GUILayout.Label(showUnitySplashScreen+ string.Format(currentValue, PlayerSettings.showUnitySplashScreen));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_ShowUnitySplashScreen)))
{
PlayerSettings.showUnitySplashScreen= recommended_ShowUnitySplashScreen;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ showUnitySplashScreen, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore +defaultIsFullScreen) &&
PlayerSettings.defaultIsFullScreen!= recommended_DefaultIsFullScreen)
{
++numItems;
GUILayout.Label(defaultIsFullScreen+ string.Format(currentValue, PlayerSettings.defaultIsFullScreen));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_DefaultIsFullScreen)))
{
PlayerSettings.defaultIsFullScreen= recommended_DefaultIsFullScreen;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ defaultIsFullScreen, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore +defaultScreenSize) &&
(PlayerSettings.defaultScreenWidth!= recommended_DefaultScreenWidth ||
PlayerSettings.defaultScreenHeight!= recommended_DefaultScreenHeight))
{
++numItems;
GUILayout.Label(defaultScreenSize+ string.Format(" ({0}x{ 1})", PlayerSettings.defaultScreenWidth, PlayerSettings.defaultScreenHeight));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format("Userecommended ({ 0}x{ 1})",recommended_DefaultScreenWidth, recommended_DefaultScreenHeight)))
{
PlayerSettings.defaultScreenWidth= recommended_DefaultScreenWidth;
PlayerSettings.defaultScreenHeight= recommended_DefaultScreenHeight;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ defaultScreenSize, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore +runInBackground) &&
PlayerSettings.runInBackground!= recommended_RunInBackground)
{
++numItems;
GUILayout.Label(runInBackground+ string.Format(currentValue, PlayerSettings.runInBackground));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_RunInBackground)))
{
PlayerSettings.runInBackground= recommended_RunInBackground;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ runInBackground, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore +displayResolutionDialog) &&
PlayerSettings.displayResolutionDialog!= recommended_DisplayResolutionDialog)
{
++numItems;
GUILayout.Label(displayResolutionDialog+ string.Format(currentValue, PlayerSettings.displayResolutionDialog));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_DisplayResolutionDialog)))
{
PlayerSettings.displayResolutionDialog= recommended_DisplayResolutionDialog;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ displayResolutionDialog, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore +resizableWindow) &&
PlayerSettings.resizableWindow!= recommended_ResizableWindow)
{
++numItems;
GUILayout.Label(resizableWindow+ string.Format(currentValue, PlayerSettings.resizableWindow));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_ResizableWindow)))
{
PlayerSettings.resizableWindow= recommended_ResizableWindow;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ resizableWindow, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore + fullscreenMode)&&
PlayerSettings.d3d11FullscreenMode!= recommended_FullscreenMode)
{
++numItems;
GUILayout.Label(fullscreenMode+ string.Format(currentValue, PlayerSettings.d3d11FullscreenMode));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_FullscreenMode)))
{
PlayerSettings.d3d11FullscreenMode= recommended_FullscreenMode;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ fullscreenMode, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore +visibleInBackground) &&
PlayerSettings.visibleInBackground!= recommended_VisibleInBackground)
{
++numItems;
GUILayout.Label(visibleInBackground+ string.Format(currentValue, PlayerSettings.visibleInBackground));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_VisibleInBackground)))
{
PlayerSettings.visibleInBackground= recommended_VisibleInBackground;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ visibleInBackground, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore + renderingPath)&&
PlayerSettings.renderingPath!= recommended_RenderPath)
{
++numItems;
GUILayout.Label(renderingPath+ string.Format(currentValue, PlayerSettings.renderingPath));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_RenderPath) + " - required for MSAA"))
{
PlayerSettings.renderingPath= recommended_RenderPath;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ renderingPath, true);
}
GUILayout.EndHorizontal();
}
if (!EditorPrefs.HasKey(ignore + colorSpace)&&
PlayerSettings.colorSpace!= recommended_ColorSpace)
{
++numItems;
GUILayout.Label(colorSpace+ string.Format(currentValue, PlayerSettings.colorSpace));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_ColorSpace) + " - requires reloadingscene"))
{
PlayerSettings.colorSpace= recommended_ColorSpace;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ colorSpace, true);
}
GUILayout.EndHorizontal();
}
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 ||UNITY_5_0)
if(!EditorPrefs.HasKey(ignore + gpuSkinning) &&
PlayerSettings.gpuSkinning!= recommended_GpuSkinning)
{
++numItems;
GUILayout.Label(gpuSkinning+ string.Format(currentValue, PlayerSettings.gpuSkinning));
GUILayout.BeginHorizontal();
if(GUILayout.Button(string.Format(useRecommended, recommended_GpuSkinning)))
{
PlayerSettings.gpuSkinning= recommended_GpuSkinning;
}
GUILayout.FlexibleSpace();
if(GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ gpuSkinning, true);
}
GUILayout.EndHorizontal();
}
#endif
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0) && false
if(!EditorPrefs.HasKey(ignore + singlePassStereoRendering) &&
PlayerSettings.singlePassStereoRendering!= recommended_SinglePassStereoRendering)
{
++numItems;
GUILayout.Label(singlePassStereoRendering+ string.Format(currentValue, PlayerSettings.singlePassStereoRendering));
GUILayout.BeginHorizontal();
if(GUILayout.Button(string.Format(useRecommended,recommended_SinglePassStereoRendering)))
{
PlayerSettings.singlePassStereoRendering= recommended_SinglePassStereoRendering;
}
GUILayout.FlexibleSpace();
if(GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ singlePassStereoRendering, true);
}
GUILayout.EndHorizontal();
}
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
if (!EditorPrefs.HasKey(ignore +stereoscopicRendering) &&
PlayerSettings.stereoscopic3D!= recommended_StereoscopicRendering)
{
++numItems;
GUILayout.Label(stereoscopicRendering+ string.Format(currentValue, PlayerSettings.stereoscopic3D));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_StereoscopicRendering)))
{
PlayerSettings.stereoscopic3D= recommended_StereoscopicRendering;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ stereoscopicRendering, true);
}
GUILayout.EndHorizontal();
}
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1)
if (!EditorPrefs.HasKey(ignore +virtualRealitySupported) &&
PlayerSettings.virtualRealitySupported!= recommended_VirtualRealitySupported)
{
++numItems;
GUILayout.Label(virtualRealitySupported+ string.Format(currentValue, PlayerSettings.virtualRealitySupported));
GUILayout.BeginHorizontal();
if (GUILayout.Button(string.Format(useRecommended,recommended_VirtualRealitySupported)))
{
PlayerSettings.virtualRealitySupported= recommended_VirtualRealitySupported;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Ignore"))
{
EditorPrefs.SetBool(ignore+ virtualRealitySupported, true);
}
GUILayout.EndHorizontal();
}
#endif
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
在右下角加一个“Clear All Ignores”按钮
if (GUILayout.Button("ClearAll Ignores"))
{
作用是清理所有用户选择“忽略”了的配置项,以让用户重新决定
EditorPrefs.DeleteKey(ignore+ buildTarget);
EditorPrefs.DeleteKey(ignore+ showUnitySplashScreen);
EditorPrefs.DeleteKey(ignore+ defaultIsFullScreen);
EditorPrefs.DeleteKey(ignore+ defaultScreenSize);
EditorPrefs.DeleteKey(ignore+ runInBackground);
EditorPrefs.DeleteKey(ignore+ displayResolutionDialog);
EditorPrefs.DeleteKey(ignore+ resizableWindow);
EditorPrefs.DeleteKey(ignore+ fullscreenMode);
EditorPrefs.DeleteKey(ignore+ visibleInBackground);
EditorPrefs.DeleteKey(ignore+ renderingPath);
EditorPrefs.DeleteKey(ignore+ colorSpace);
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 ||UNITY_5_0)
EditorPrefs.DeleteKey(ignore+ gpuSkinning);
#endif
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0) && false
EditorPrefs.DeleteKey(ignore+ singlePassStereoRendering);
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
EditorPrefs.DeleteKey(ignore+ stereoscopicRendering);
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1)
EditorPrefs.DeleteKey(ignore+ virtualRealitySupported);
#endif
}
GUILayout.EndHorizontal();
GUILayout.EndScrollView();
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
if (numItems > 0)
{
最后添加一个“Accept All”按钮用于接受所有推荐的配置项,添加一个 “Ignore All”按钮用于忽略所有有冲突的配置项(使用缺省的配置项)
if (GUILayout.Button("AcceptAll"))
{
//Only set those that have not been explicitly ignored.
也不是接受所有了,如果用户已经明确选择了忽略则还是会被忽略
if (!EditorPrefs.HasKey(ignore + buildTarget))
EditorUserBuildSettings.SwitchActiveBuildTarget(recommended_BuildTarget);
if (!EditorPrefs.HasKey(ignore +showUnitySplashScreen))
PlayerSettings.showUnitySplashScreen= recommended_ShowUnitySplashScreen;
if (!EditorPrefs.HasKey(ignore +defaultIsFullScreen))
PlayerSettings.defaultIsFullScreen= recommended_DefaultIsFullScreen;
if (!EditorPrefs.HasKey(ignore +defaultScreenSize))
{
PlayerSettings.defaultScreenWidth= recommended_DefaultScreenWidth;
PlayerSettings.defaultScreenHeight= recommended_DefaultScreenHeight;
}
if (!EditorPrefs.HasKey(ignore +runInBackground))
PlayerSettings.runInBackground= recommended_RunInBackground;
if (!EditorPrefs.HasKey(ignore +displayResolutionDialog))
PlayerSettings.displayResolutionDialog= recommended_DisplayResolutionDialog;
if (!EditorPrefs.HasKey(ignore +resizableWindow))
PlayerSettings.resizableWindow= recommended_ResizableWindow;
if (!EditorPrefs.HasKey(ignore +fullscreenMode))
PlayerSettings.d3d11FullscreenMode= recommended_FullscreenMode;
if (!EditorPrefs.HasKey(ignore +visibleInBackground))
PlayerSettings.visibleInBackground= recommended_VisibleInBackground;
if (!EditorPrefs.HasKey(ignore + renderingPath))
PlayerSettings.renderingPath= recommended_RenderPath;
if (!EditorPrefs.HasKey(ignore + colorSpace))
PlayerSettings.colorSpace= recommended_ColorSpace;
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 ||UNITY_5_0)
if(!EditorPrefs.HasKey(ignore + gpuSkinning))
PlayerSettings.gpuSkinning= recommended_GpuSkinning;
#endif
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0) && false
if(!EditorPrefs.HasKey(ignore + singlePassStereoRendering))
PlayerSettings.singlePassStereoRendering= recommended_SinglePassStereoRendering;
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
if (!EditorPrefs.HasKey(ignore +stereoscopicRendering))
PlayerSettings.stereoscopic3D= recommended_StereoscopicRendering;
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1)
if (!EditorPrefs.HasKey(ignore +virtualRealitySupported))
PlayerSettings.virtualRealitySupported= recommended_VirtualRealitySupported;
#endif
EditorUtility.DisplayDialog("AcceptAll", "You made the right choice!", "Ok");
这个看起来是关闭窗口
Close();
}
if (GUILayout.Button("IgnoreAll"))
{
看起来这是弹一个类似Windows的MessageBox和Android的 AlertDialog的方法
if (EditorUtility.DisplayDialog("IgnoreAll", "Are you sure?", "Yes, Ignore All", "Cancel"))
{
//Only ignore those that do not currently match our recommended settings.
if (EditorUserBuildSettings.activeBuildTarget !=recommended_BuildTarget)
EditorPrefs.SetBool(ignore+ buildTarget, true);
if (PlayerSettings.showUnitySplashScreen !=recommended_ShowUnitySplashScreen)
EditorPrefs.SetBool(ignore+ showUnitySplashScreen, true);
if (PlayerSettings.defaultIsFullScreen !=recommended_DefaultIsFullScreen)
EditorPrefs.SetBool(ignore+ defaultIsFullScreen, true);
if (PlayerSettings.defaultScreenWidth !=recommended_DefaultScreenWidth ||
PlayerSettings.defaultScreenHeight!= recommended_DefaultScreenHeight)
EditorPrefs.SetBool(ignore+ defaultScreenSize, true);
if (PlayerSettings.runInBackground !=recommended_RunInBackground)
EditorPrefs.SetBool(ignore+ runInBackground, true);
if (PlayerSettings.displayResolutionDialog !=recommended_DisplayResolutionDialog)
EditorPrefs.SetBool(ignore+ displayResolutionDialog, true);
if (PlayerSettings.resizableWindow !=recommended_ResizableWindow)
EditorPrefs.SetBool(ignore+ resizableWindow, true);
if (PlayerSettings.d3d11FullscreenMode !=recommended_FullscreenMode)
EditorPrefs.SetBool(ignore+ fullscreenMode, true);
if (PlayerSettings.visibleInBackground !=recommended_VisibleInBackground)
EditorPrefs.SetBool(ignore+ visibleInBackground, true);
if (PlayerSettings.renderingPath !=recommended_RenderPath)
EditorPrefs.SetBool(ignore+ renderingPath, true);
if (PlayerSettings.colorSpace !=recommended_ColorSpace)
EditorPrefs.SetBool(ignore+ colorSpace, true);
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 ||UNITY_5_0)
if(PlayerSettings.gpuSkinning != recommended_GpuSkinning)
EditorPrefs.SetBool(ignore+ gpuSkinning, true);
#endif
#if !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0) && false
if(PlayerSettings.singlePassStereoRendering !=recommended_SinglePassStereoRendering)
EditorPrefs.SetBool(ignore+ singlePassStereoRendering, true);
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
if (PlayerSettings.stereoscopic3D !=recommended_StereoscopicRendering)
EditorPrefs.SetBool(ignore+ stereoscopicRendering, true);
#endif
#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1)
if (PlayerSettings.virtualRealitySupported !=recommended_VirtualRealitySupported)
EditorPrefs.SetBool(ignore+ virtualRealitySupported, true);
#endif
Close();
}
}
}
else if (GUILayout.Button("Close"))
{
如果所有的推荐配置都和缺省配置一样,则只会显示一个“Close”按钮
Close();
}
GUILayout.EndHorizontal();
}
}
4.4. SteamVR_SkyboxEditor.cs
针对SteamVR_Skybox.cs的定制Inspector属性显示的脚本,之所以提供这个脚本是为了提供截图的功能,并且可以直接将截图赋给SkyBox的六个面,否则使用缺省的Inspector显示就可以了。这个SteamVR_Skybox的作用是为合成器提供立方体帖图的,用在场景过渡等地方,参看后面的SteamVR_Skybox.cs。
不过看SteamVR_Skybox.cs的内容,它只是在OnEnable的时候往合成器设置了skybox,因此与场景关联不大,可以把脚本作为组件添加到任何场景中的物体上,比如添加到相机上。它的界面是这样的:
它的作用是通过截图生成Skybox 6个方向的纹理贴图。翻译一下上图的提示部分,有利于理解其作用和原理:
截图会根据当前的位置和旋转来截取6个方向的截图作为skybox的纹理。注意:这个天空盒仅用来覆盖合成器中出现的天空盒(比如,当进行关卡加载时)。添加一个相机到这个物体(即SteamVR_Skybox脚本所在的物体)上以覆盖缺省的设置,比如渲染哪些层。此外,通过指定你自己的targetTexture,你可以控制纹理的大小及其它比如反走样之类的属性。不要忘了禁用这个相机。
对于立体截图,会使用指定的ipd(瞳间距,单位毫米)为每只眼睛渲染一幅全景图,整幅图像会被划分成cellSize大小的方块以优化生成过程。根据场景的复杂程度,32x32的网格大小要花大概10秒钟时间,16x16大概1分钟,8x8要花好几分钟。
要测试这个过程,可以先点play然后再点pause,这样就会激活天空盒的设置,然后你就可以看到渲染了天空盒的合成器了。
同所有的Inspector定制一样,使用CustomEditor和CanEditMultipleObjects属性
[CustomEditor(typeof(SteamVR_Skybox)), CanEditMultipleObjects]
public class SteamVR_SkyboxEditor : Editor
{
保存的截图命名格式:场景文件所在路径/场景名字/脚本所在物体名称-6个方向序 号.png
private const string nameFormat = "{0}/{1}-{2}.png";
private const string helpText = "Takesnapshot will use the current " +
"positionand rotation to capture six directional screenshots to use as this " +
"skybox'stextures. Note: This skybox is only used to override what shows up " +
"inthe compositor (e.g. when loading levels). Add a Camera component to this" +
"objectto override default settings like which layers to render. Additionally," +
"byspecifying your own targetTexture, you can control the size of the textures" +
"andother properties like antialiasing. Don't forget to disable the camera.nn" +
"Forstereo screenshots, a panorama is render for each eye using the specified" +
"ipd(in millimeters) broken up into segments cellSize pixels square to optimize" +
"generation.n(32x32 takes about 10 seconds depending on scene complexity,16x16 " +
"takesaround a minute, while will 8x8 take several minutes.) nnTotest, hit " +
"playthen pause - this will activate the skybox settings, and then drop you to" +
"thecompositor where the skybox is rendered.";
绘制UI
public override void OnInspectorGUI()
{
这次没有定制了,而调用了基类的方法,这样前面部分就是缺省的样子。所以可以 看到Script选项是灰的。前面几个自定义绘制的Inspector中Script选项是可 以修改的。
DrawDefaultInspector();
#if !(UNITY_5_0 || UNITY_5_1)
非5.0、5.1版本显示上面的提示信息
EditorGUILayout.HelpBox(helpText, MessageType.Info);
显示“Take snapshot”按钮
if (GUILayout.Button("Takesnapshot"))
{
#if (UNITY_5_2)
5.2与其它版本获取场景路径的方式不一样
获取场景名字(不带扩展名)
varsceneName = Path.GetFileNameWithoutExtension(EditorApplication.currentScene);
获取场景路径
varscenePath = Path.GetDirectoryName(EditorApplication.currentScene);
varassetPath = scenePath +"/" + sceneName;
if(!AssetDatabase.IsValidFolder(assetPath))
{
创建目录返回的是一个guid。因为从名字看所有的asset是有一个数据 库来维护的(AssetDatabase),guid刚好可以做关键字。
varguid = AssetDatabase.CreateFolder(scenePath, sceneName);
从guid获取路径
assetPath= AssetDatabase.GUIDToAssetPath(guid);
}
#endif
6个方向,采用四元数表示 TODO 关于四元数,还要再看一下细节
var directions = new Quaternion[]{
Quaternion.LookRotation(Vector3.forward),
Quaternion.LookRotation(Vector3.back),
Quaternion.LookRotation(Vector3.left),
Quaternion.LookRotation(Vector3.right),
Quaternion.LookRotation(Vector3.up, Vector3.back),
Quaternion.LookRotation(Vector3.down, Vector3.forward)
};
Camera tempCamera = null;
foreach (SteamVR_Skybox target in targets)
{
#if !(UNITY_5_2)
非5.2版本获取场景路径的做法。是从SteamVR_Skybox所在的物体所 在的场景来获取的。5.2的做法是根据当前场景来取的。
var targetScene = target.gameObject.scene;
var sceneName = Path.GetFileNameWithoutExtension(targetScene.path);
var scenePath = Path.GetDirectoryName(targetScene.path);
var assetPath = scenePath + "/" + sceneName;
if (!AssetDatabase.IsValidFolder(assetPath))
{
var guid = AssetDatabase.CreateFolder(scenePath,sceneName);
assetPath= AssetDatabase.GUIDToAssetPath(guid);
}
#endif
从这里看,最好就是将SteamVR_Skybox脚本添加到一个Camera对象上
var camera = target.GetComponent<Camera>();
if (camera == null)
{
如果SteamVR_Skybox所在的对象没有Camera,则创建一个临时的 Camera。注意这里并没有加到SteamVR_Skybox脚本所在的物体上, 而是新建了一个空的GameObject
if (tempCamera == null)
tempCamera= new GameObject().AddComponent<Camera>();
camera= tempCamera;
}
var targetTexture = camera.targetTexture;
if (camera.targetTexture == null)
{
如果相机上没有targetTexture,则添加一个
targetTexture= new RenderTexture(1024, 1024, 24);
targetTexture.antiAliasing= 8;
camera.targetTexture= targetTexture;
}
保存当前的位置信息,后面会修改并恢复
var oldPosition = target.transform.localPosition;
var oldRotation = target.transform.localRotation;
var baseRotation = target.transform.rotation;
var t = camera.transform;
t.position= target.transform.position;
不使用正交投影(即使用透视投影)
camera.orthographic= false;
视角90度?—— 这是透视投影相机的一个参数
camera.fieldOfView= 90;
for (int i = 0;i < directions.Length; i++)
{
生成6个方向的截图。下面是截图的方法:调整相机位置,然后调用 相机的Render,然后创建2D纹理,然后调用texture.ReadPixels 就完成的截图。所以这里的截图就是截从相机中看到的景象
TODO 关于四元数的乘法的意义还要再看
t.rotation= baseRotation * directions[i];
camera.Render();
//Copy to texture and save to disk.
RenderTexture.active= targetTexture;
var texture = new Texture2D(targetTexture.width,targetTexture.height, TextureFormat.ARGB32, false);
texture.ReadPixels(new Rect(0, 0,texture.width, texture.height), 0, 0);
texture.Apply();
RenderTexture.active= null;
保存到文件,文件格式为png,位置为:当前场景文件所有目录/场 景名称/脚本所在物体名称-方向序号.png
var assetName = string.Format(nameFormat, assetPath,target.name, i);
System.IO.File.WriteAllBytes(assetName,texture.EncodeToPNG());
}
if (camera != tempCamera)
{
如果不是临时相机,还原物体的原始位置
target.transform.localPosition= oldPosition;
target.transform.localRotation= oldRotation;
}
}
if (tempCamera != null)
{
删除临时相机。这个类似于Windows GDI对象。因为C#对象和java对 象一样是自动回收的,回收时机不定,对于大对象,强制资源回收是有必 要的
Object.DestroyImmediate(tempCamera.gameObject);
}
//Now that everything has be written out, reload the associated assets and assignthem.
下面是将上面生成的截图当作纹理添加到SteamVR_Skybox的6个方向的纹 理上
刚保存了文件,这里刷新Asset数据库
AssetDatabase.Refresh();
foreach (SteamVR_Skybox target in targets)
{
#if !(UNITY_5_2)
var targetScene = target.gameObject.scene;
var sceneName = Path.GetFileNameWithoutExtension(targetScene.path);
var scenePath = Path.GetDirectoryName(targetScene.path);
var assetPath = scenePath + "/" + sceneName;
#endif
for (int i = 0;i < directions.Length; i++)
{
var assetName = string.Format(nameFormat, assetPath,target.name, i);
导入上面生成的截图作为纹理
var importer = AssetImporter.GetAtPath(assetName) as TextureImporter;
importer.textureFormat= TextureImporterFormat.RGB24;
importer.wrapMode= TextureWrapMode.Clamp;
importer.mipmapEnabled= false;
importer.SaveAndReimport();
把纹理设置到相应的SteamVR_Skybox的纹理上
var texture = AssetDatabase.LoadAssetAtPath<Texture>(assetName);
target.SetTextureByIndex(i,texture);
}
}
}
else if (GUILayout.Button("Takestereo snapshot"))
{
生成立体截图。这个只会生成左右两只眼的两幅截图,大概是因为这是生成左 右眼的立体全景图,应该就是每只眼睛所能看到的1/4半球的场景。TODO 下 面有很复杂的数学计算,暂时看不懂,后面再研究。
生成的截图的大小为4096x2048
const int width = 4096;
生成的截图只是一个半球,地面以下是看不到的
const int height = width / 2;
const int halfHeight = height / 2;
保存左右眼两幅图像的纹理数组
var textures = new Texture2D[]{
new Texture2D(width, height, TextureFormat.ARGB32, false),
new Texture2D(width, height, TextureFormat.ARGB32, false) };
统计耗时的,在Start/Stop之间
var timer = new System.Diagnostics.Stopwatch();
Camera tempCamera = null;
foreach (SteamVR_Skybox target in targets)
{
timer.Start();
#if !(UNITY_5_2)
与上面的图像保存的位置和命名格式是一样的。所以这两个功能不能同时 使用,文件会被覆盖。
var targetScene = target.gameObject.scene;
var sceneName = Path.GetFileNameWithoutExtension(targetScene.path);
var scenePath = Path.GetDirectoryName(targetScene.path);
var assetPath = scenePath + "/" + sceneName;
if (!AssetDatabase.IsValidFolder(assetPath))
{
var guid = AssetDatabase.CreateFolder(scenePath,sceneName);
assetPath= AssetDatabase.GUIDToAssetPath(guid);
}
#endif
仍然是要绑定一个相机
var camera = target.GetComponent<Camera>();
if (camera == null)
{
if (tempCamera == null)
tempCamera= new GameObject().AddComponent<Camera>();
camera= tempCamera;
}
在相机所在的物体上添加SteamVR_SphericalProjection脚本。这个 脚本从名字上看是用来做球形投影的(大概就是把比如一个纹理投影到一 个球上)
var fx = camera.gameObject.AddComponent<SteamVR_SphericalProjection>();
保存参数,后面恢复
var oldTargetTexture = camera.targetTexture;
var oldOrthographic = camera.orthographic;
var oldFieldOfView = camera.fieldOfView;
var oldAspect = camera.aspect;
var oldPosition = target.transform.localPosition;
var oldRotation = target.transform.localRotation;
var basePosition = target.transform.position;
var baseRotation = target.transform.rotation;
var transform = camera.transform;
cellSize是SteamVR_Skybox中的一个参数,用户可以在Insepctor 中修改,缺省是32x32的。它的作用是将整幅图像分隔cellSize大小 的方块来处理
cellSize的格式为“x32”这样的格式
int cellSize = int.Parse(target.StereoCellSize.ToString().Substring(1));
瞳间距,以米为单位,在界面上是以毫米为单位
float ipd = target.StereoIpdMm / 1000.0f;
垂直方向的(一半)网格数(或者称为段数,segment)
int vTotal = halfHeight / cellSize;
垂直方向每段度数(总共180度,一半90度)
float dv = 90.0f / vTotal; // verticaldegrees per segment
float dvHalf = dv / 2.0f;
看样子是一个网格一个网格渲染
var targetTexture = new RenderTexture(cellSize,cellSize, 24);
targetTexture.wrapMode= TextureWrapMode.Clamp;
targetTexture.antiAliasing= 8;
每次渲染的视角
camera.fieldOfView= dv;
camera.orthographic= false;
camera.targetTexture= targetTexture;
//Render sections of a sphere using a rectilinear projection
//and resample using a sphereical projection into a single panorama
//texture per eye. We break into sections in order to keep the eye
//separation similar around the sphere. Rendering alternates between
//top and bottom sections, sweeping horizontally around the sphere,
//alternating left and right eyes.
for (int v = 0;v < vTotal; v++)
{
var pitch = 90.0f - (v * dv) - dvHalf;
var uTotal = width / targetTexture.width;
var du = 360.0f / uTotal; // horizontal degrees per segment
var duHalf = du / 2.0f;
var vTarget = v * halfHeight / vTotal;
for (int i = 0;i < 2; i++) // top,bottom
{
if (i == 1)
{
pitch= -pitch;
vTarget= height - vTarget - cellSize;
}
for (int u = 0;u < uTotal; u++)
{
var yaw = -180.0f + (u * du) + duHalf;
var uTarget = u * width / uTotal;
var xOffset = -ipd / 2 * Mathf.Cos(pitch* Mathf.Deg2Rad);
for (int j = 0;j < 2; j++) // left,right
{
var texture = textures[j];
if (j == 1)
{
xOffset= -xOffset;
}
var offset = baseRotation * Quaternion.Euler(0,yaw, 0) * new Vector3(xOffset, 0, 0);
transform.position= basePosition + offset;
var direction = Quaternion.Euler(pitch, yaw, 0.0f);
transform.rotation= baseRotation * direction;
//vector pointing to center of this section
var N = direction * Vector3.forward;
//horizontal span of this section in degrees
var phi0 = yaw - (du / 2);
var phi1 = phi0 + du;
//vertical span of this section in degrees
var theta0 = pitch + (dv / 2);
var theta1 = theta0 - dv;
var midPhi = (phi0 + phi1) / 2;
var baseTheta = Mathf.Abs(theta0) < Mathf.Abs(theta1)? theta0 : theta1;
//vectors pointing to corners of image closes to the equator
var V00 = Quaternion.Euler(baseTheta, phi0, 0.0f)* Vector3.forward;
var V01 = Quaternion.Euler(baseTheta, phi1, 0.0f)* Vector3.forward;
//vectors pointing to top and bottom midsection of image
var V0M = Quaternion.Euler(theta0, midPhi, 0.0f)* Vector3.forward;
var V1M = Quaternion.Euler(theta1, midPhi, 0.0f)* Vector3.forward;
//intersection points for each of the above
var P00 = V00 / Vector3.Dot(V00, N);
var P01 = V01 / Vector3.Dot(V01, N);
var P0M = V0M / Vector3.Dot(V0M, N);
var P1M = V1M / Vector3.Dot(V1M, N);
//calculate basis vectors for plane
var P00_P01 = P01 - P00;
var P0M_P1M = P1M - P0M;
var uMag = P00_P01.magnitude;
var vMag = P0M_P1M.magnitude;
var uScale = 1.0f / uMag;
var vScale = 1.0f / vMag;
var uAxis = P00_P01 * uScale;
var vAxis = P0M_P1M * vScale;
//update material constant buffer
fx.Set(N,phi0, phi1, theta0, theta1,
uAxis,P00, uScale,
vAxis,P0M, vScale);
camera.aspect= uMag / vMag;
camera.Render();
RenderTexture.active= targetTexture;
texture.ReadPixels(new Rect(0, 0,targetTexture.width, targetTexture.height), uTarget, vTarget);
RenderTexture.active= null;
}
}
}
}
下面的做法与上面普通的截图是一样的了
//Save textures to disk.
for (int i = 0;i < 2; i++)
{
var texture = textures[i];
texture.Apply();
var assetName = string.Format(nameFormat, assetPath,target.name, i);
File.WriteAllBytes(assetName,texture.EncodeToPNG());
}
//Cleanup.
if (camera != tempCamera)
{
camera.targetTexture= oldTargetTexture;
camera.orthographic= oldOrthographic;
camera.fieldOfView= oldFieldOfView;
camera.aspect= oldAspect;
target.transform.localPosition= oldPosition;
target.transform.localRotation= oldRotation;
}
else
{
tempCamera.targetTexture= null;
}
DestroyImmediate(targetTexture);
DestroyImmediate(fx);
timer.Stop();
Debug.Log(string.Format("Screenshottook { 0} seconds.",timer.Elapsed));
}
if (tempCamera != null)
{
DestroyImmediate(tempCamera.gameObject);
}
DestroyImmediate(textures[0]);
DestroyImmediate(textures[1]);
//Now that everything has be written out, reload the associated assets and assignthem.
AssetDatabase.Refresh();
foreach (SteamVR_Skybox target in targets)
{
#if !(UNITY_5_2)
var targetScene = target.gameObject.scene;
var sceneName = Path.GetFileNameWithoutExtension(targetScene.path);
var scenePath = Path.GetDirectoryName(targetScene.path);
var assetPath = scenePath + "/" + sceneName;
#endif
for (int i = 0;i < 2; i++)
{
var assetName = string.Format(nameFormat, assetPath,target.name, i);
var importer = AssetImporter.GetAtPath(assetName) as TextureImporter;
importer.mipmapEnabled= false;
importer.wrapMode= TextureWrapMode.Repeat;
importer.SetPlatformTextureSettings("Standalone",width, TextureImporterFormat.RGB24);
importer.SaveAndReimport();
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetName);
TODO 这里为什么把左右眼的全景图设为Skybox的前、后面的纹理
target.SetTextureByIndex(i,texture);
}
}
}
#endif
}
}
4.5. SteamVR_Update.cs
这个脚本用于检查SteamVR的Unity插件是否有升级并提示用户。如图(通过更改cs文件中的版本号强制显示):
脚本在启动的时候(第一次安装也会)会调用
[InitializeOnLoad]
public class SteamVR_Update : EditorWindow
{
const string currentVersion = "1.1.0";
采用了极其简单的版本控制系统,在自己的官网用一个文本文件描述了最新的版本
const string versionUrl = "http://media.steampowered.com/apps/steamvr/unitypluginversion.txt";
有一个URL描述版本信息
const string notesUrl = "http://media.steampowered.com/apps/steamvr/unityplugin-v{0}.txt";
插件的下载地址为unity3d的官网
const string pluginUrl = "http://u3d.as/content/valve-corporation/steam-vr-plugin";
忽略更新的信息保存在EditorPref中,这个是键值
const string doNotShowKey = "SteamVR.DoNotShow.v{0}";
static WWW wwwVersion, wwwNotes;
static string version, notes;
static SteamVR_Update window;
static SteamVR_Update()
{
使用WWW类来做http请求
wwwVersion= new WWW(versionUrl);
将Update方法添加定期更新委托链中
EditorApplication.update+= Update;
}
static void Update()
{
if (wwwVersion != null)
{
判断请求是否完成。因为Update会被持续调用,所以一直等到请求完成才会 继续
if (!wwwVersion.isDone)
return;
判断请求是否成功
if (UrlSuccess(wwwVersion))
版本信息就保存在网页的原始内容里
version= wwwVersion.text;
wwwVersion= null;
根据版本信息对比判断是否需要升级
if (ShouldDisplay())
{
再去取升级信息
var url = string.Format(notesUrl, version);
wwwNotes= new WWW(url);
调用GetWindow,就会将当前窗口显示出来
window= GetWindow<SteamVR_Update>(true);
window.minSize= new Vector2(320, 440);
//window.title= "SteamVR";
}
}
if (wwwNotes != null)
{
if (!wwwNotes.isDone)
return;
if (UrlSuccess(wwwNotes))
notes= wwwNotes.text;
wwwNotes= null;
if (notes != "")
取到更新信息了,请求窗口重绘以显示拉取到的更新信息
window.Repaint();
}
从更新委托链中去掉当前方法,将不再调用Update方法
EditorApplication.update-= Update;
}
static bool UrlSuccess(WWW www)
{
判断http请求是否成功,依据是没有出现错误代码,并且返回的内容中不是404 页面
if (!string.IsNullOrEmpty(www.error))
return false;
if (Regex.IsMatch(www.text, "404not found", RegexOptions.IgnoreCase))
return false;
return true;
}
判断是否显示升级信息
static bool ShouldDisplay()
{
if (string.IsNullOrEmpty(version))
没有拉取到版本信息
return false;
if (version == currentVersion)
当前版本就是最新版本
return false;
if (EditorPrefs.HasKey(string.Format(doNotShowKey,version)))
当前版本的更新被忽略
return false;
//parse to see if newer (e.g. 1.0.4 vs 1.0.3)
对版本号进行解析。实际上可以直接比较字符串的大小,只要判断号的命名规则一 致
var versionSplit = version.Split('.');
var currentVersionSplit = currentVersion.Split('.');
for (int i = 0;i < versionSplit.Length && i < currentVersionSplit.Length; i++)
{
int versionValue, currentVersionValue;
if (int.TryParse(versionSplit[i], out versionValue) &&
int.TryParse(currentVersionSplit[i], out currentVersionValue))
{
if (versionValue > currentVersionValue)
return true;
if (versionValue < currentVersionValue)
return false;
}
}
//same up to this point, now differentiate based on number of sub values (e.g.1.0.4.1 vs 1.0.4)
if (versionSplit.Length <= currentVersionSplit.Length)
return false;
return true;
}
Vector2 scrollPosition;
bool toggleState;
仍然是获取logo图片所有目录(Texture目录)
string GetResourcePath()
{
var ms = MonoScript.FromScriptableObject(this);
var path = AssetDatabase.GetAssetPath(ms);
path = Path.GetDirectoryName(path);
return path.Substring(0, path.Length - "Editor".Length)+ "Textures/";
}
绘制界面
public void OnGUI()
{
显示一条提示
EditorGUILayout.HelpBox("Anew version of the SteamVR plugin is available!", MessageType.Warning);
显示logo
var resourcePath = GetResourcePath();
#if UNITY_5_0
varlogo = Resources.LoadAssetAtPath
var logo = AssetDatabase.LoadAssetAtPath<Texture2D>(resourcePath+ "logo.png");
#endif
var rect = GUILayoutUtility.GetRect(position.width, 150, GUI.skin.box);
if (logo)
GUI.DrawTexture(rect,logo, ScaleMode.ScaleToFit);
开始scrollview
scrollPosition= GUILayout.BeginScrollView(scrollPosition);
显示当前版本
GUILayout.Label("Currentversion: " + currentVersion);
显示最新版本
GUILayout.Label("Newversion: " + version);
if (notes != "")
{
显示更新提示
GUILayout.Label("Releasenotes:");
EditorGUILayout.HelpBox(notes, MessageType.Info);
}
ScrollView结束
GUILayout.EndScrollView();
让下面的按钮底部对齐
GUILayout.FlexibleSpace();
if (GUILayout.Button("GetLatest Version"))
{
使用Unity的OpenURL打开URL,使用的是浏览器打开(其实使用AssetStore 打开不是更好吗)
Application.OpenURL(pluginUrl);
}
显示一个忽略当前更新的CheckBox
通过EditorGUI.BeginChangeCheck/EndChangeCheck来监听是否点击了 CheckBox。一定要通过这样的方式吗?CheckBox没有自己的事件?
EditorGUI.BeginChangeCheck();
GUILayout.Toggle即CheckBox
var doNotShow = GUILayout.Toggle(toggleState, "Donot prompt for this version again.");
if (EditorGUI.EndChangeCheck())
{
toggleState= doNotShow;
var key = string.Format(doNotShowKey, version);
if (doNotShow)
EditorPrefs.SetBool(key, true);
else
EditorPrefs.DeleteKey(key);
}
}
}