SteamVR(HTC Vive) Unity插件深度分析(十三)
10.18. SteamVR_RenderModel.cs
跟踪设备的渲染模型,就是将跟踪设备(特别是手柄)模型显示出来。在SteamVR的例子中,它被挂在了左右Controller下面的Model上:
在SteamVR示例Extras/SteamVR_TestThrow.unity这个场景中有SteamVR_RenderModel的完整示例(模型是自动/动态创建的,所以在上面的截图中,只要运行了,也能看到下图的模型):
可以看到Model下面有很多的子组件,而每个子组件下面都有一个attach对象。
在编辑器里也能显示
[ExecuteInEditMode]
public class SteamVR_RenderModel : MonoBehaviour
{
跟踪设备的索引
public SteamVR_TrackedObject.EIndex index = SteamVR_TrackedObject.EIndex.None;
这个是用于覆盖的渲染模型的名字(也可以是本地渲染模型的全路径)。它在运行时并 不能使用,能够想到它的作用是可以在编辑模式下切换索引查看所有的跟踪设备的模型
public string modelOverride;
// Shader to apply to model.
模型所使用的shader
public Shader shader;
// Enable to print out when render models are loaded.
用于调试,为true可以在加载模型时打印一些详细信息
public bool verbose = false;
// If available, break down into separate components instead ofloading as a single mesh.
如果可以,会拆分成多个单独的组件而不是单一的网格(诸如扳机这样的就能单独动了)
public bool createComponents = true;
// Update transforms of components at runtime to reflect useraction.
根据用户的动作实时更新组件的位置
public bool updateDynamically = true;
// Additional controller settings for showing scrollwheel, etc.
目前里面只有一个参数,就就是是否显示滚轮
public RenderModel_ControllerMode_State_t controllerModeState;
// Name of the sub-object which represents the "local"coordinate space for each component.
作为每个组件的子节点的名字,表示组件的局部坐标
public const string k_localTransformName = "attach";
// Cached name of this render model for updating componenttransforms at runtime.
渲染模型的名字(或者本地全路径)
public string renderModelName { get; private set; }
// If someone knows how to keep these from getting cleaned upevery time
// you exit play mode,let me know. I've tried marking the RenderModel
// class below as[System.Serializable] and switching to normal public
// variables for mesh andmaterial to get them to serialize properly,
// as well as triedmarking the mesh and material objects as
// DontUnloadUnusedAsset,but Unity was still unloading them.
// The hashtable ispreserving its entries, but the mesh and material
// variables are goingnull.
这里作者也没有处理好这个类会被Unity卸载的问题。完整注释为:
如果有人知道如何使这些对象在退出play模式时不被清理的话,请告诉我。我尝试为 下面的RenderModel类加上[System.Serializable]属性并且将mesh及material 变量声明为普通的public以使它们可以正常序列化,同时也试过将mesh和material 对象声明为DontUnloadUnusedAsset,但Unity还是会卸载它们。Hashtable会保 留相应的内容,但mesh和material会变为null
一个渲染模型就是由网格+材质组成的
public class RenderModel
{
public RenderModel(Mesh mesh, Material material)
{
this.mesh = mesh;
this.material = material;
}
网格
public Mesh mesh { get; private set; }
材质
public Material material { get; private set; }
}
models记录的是渲染模型名称与RenderModel的映射表
public static Hashtable models = new Hashtable();
Materials记录的是纹理id与Meterial的映射表
public static Hashtable materials = new Hashtable();
// Helper class to load render models interface on demand andclean up when done.
一个于辅助类,用于根据需要加载渲染模型接口及完成后清理。是作为IDisposable 类型,因此可以有自动清理功能
实际上就是对CVRRenderModels接口的获取进行封装。和之前的疑问一样, OpenVR.Init和OpenVR.Shutdown可以多次调用吗?
public sealed class RenderModelInterfaceHolder : System.IDisposable
{
private bool needsShutdown,failedLoadInterface;
private CVRRenderModels _instance;
public CVRRenderModels instance
{
get
{
if (_instance == null &&!failedLoadInterface)
{
if (!SteamVR.active && !SteamVR.usingNativeSupport)
{
var error = EVRInitError.None;
OpenVR.Init(ref error, EVRApplicationType.VRApplication_Other);
needsShutdown= true;
}
_instance= OpenVR.RenderModels;
if (_instance == null)
{
Debug.LogError("Failed to loadIVRRenderModels interface version " + OpenVR.IVRRenderModels_Version);
failedLoadInterface= true;
}
}
return _instance;
}
}
public void Dispose()
{
if (needsShutdown)
OpenVR.Shutdown();
}
}
这是“ModelSkinSettingsHaveChanged”事件的监听函数,用于监听模型皮肤的改 变。但这个事件没有看到哪里发出的通知,应该是openvr_api.dll发出的,在 SteamVR_Render.Update里面分发的
private void OnModelSkinSettingsHaveChanged(params object[] args)
{
if (!string.IsNullOrEmpty(renderModelName))
{
renderModelName = "";
调用UpdateModel更新最新的模型
UpdateModel();
}
}
“hide_render_models”事件的处理,这个会在SteamVR_Render里面发出
private void OnHideRenderModels(params object[] args)
{
根据是否隐藏渲染模型状态禁用MeshRenderer,MeshRenderer是通过代码加上 去的
bool hidden = (bool)args[0];
var meshRenderer = GetComponent<MeshRenderer>();
if (meshRenderer != null)
meshRenderer.enabled= !hidden;
foreach (var child in transform.GetComponentsInChildren<MeshRenderer>())
child.enabled =!hidden;
}
跟踪设备连接状态发生变化
private void OnDeviceConnected(params object[] args)
{
var i = (int)args[0];
if (i != (int)index)
只关注当前跟踪设备
return;
var connected = (bool)args[1];
if (connected)
{
连接上了,更新之。可以看到没有对断开连接处理。实际测试时如果在使用过 程中长按系统键关掉手柄,可以发现手柄模型还在Hierarchy列表里,只是 会被禁用(这个动作是在SteamVR_ControllerManager里面做的,设备断 开连接后会刷新控制器索引,无效的索引会被禁用)。想想之所以这样做大概 是通常情况下,没有用户会在使用过程中关掉手柄,但会有一个比较常见的情 况是手柄失去跟踪,可以预见是它很快就能恢复跟踪,所以没有必要在连接一 断开就马上销毁模型
UpdateModel();
}
}
更新模型
public void UpdateModel()
{
var system = OpenVR.System;
if (system == null)
return;
从系统中取出指定设备的模型名称,这个名称就是resources/rendermodels目 录下的模型的目录名,比如“vr_controller_vive_1_5”这个表示vive的手柄
对于缓冲区大小未知的函数调用,都是先传空参数进去以获取所需要的缓冲区大 小,然后分配缓冲区后再调用
var error = ETrackedPropertyError.TrackedProp_Success;
var capacity = system.GetStringTrackedDeviceProperty((uint)index, ETrackedDeviceProperty.Prop_RenderModelName_String, null, 0, ref error);
if (capacity <= 1)
{
Debug.LogError("Failed to getrender model name for tracked object " + index);
return;
}
var buffer = new System.Text.StringBuilder((int)capacity);
system.GetStringTrackedDeviceProperty((uint)index, ETrackedDeviceProperty.Prop_RenderModelName_String,buffer, capacity, ref error);
var s = buffer.ToString();
if (renderModelName != s)
{
模型名字发生变化,调用SetModelAsync协程来处理新的模型名字
renderModelName= s;
StartCoroutine(SetModelAsync(s));
}
}
IEnumerator SetModelAsync(string renderModelName)
{
if (string.IsNullOrEmpty(renderModelName))
yield break;
// Preload all render models beforeasking for the data to create meshes.
using (var holder = new RenderModelInterfaceHolder())
{
var renderModels =holder.instance;
if (renderModels == null)
yield break;
// Gather names of rendermodels to preload.
string[] renderModelNames;
获取指定的模型名称下面的所有组件
var count =renderModels.GetComponentCount(renderModelName);
if (count > 0)
{
renderModelNames= new string[count];
for (int i = 0; i < count; i++)
{
获取组件名称
var capacity =renderModels.GetComponentName(renderModelName, (uint)i, null, 0);
if (capacity == 0)
continue;
var componentName = new System.Text.StringBuilder((int)capacity);
if (renderModels.GetComponentName(renderModelName, (uint)i, componentName, capacity) == 0)
continue;
获取渲染模型下单个组件的渲染模型
capacity= renderModels.GetComponentRenderModelName(renderModelName,componentName.ToString(), null, 0);
if (capacity == 0)
continue;
var name = new System.Text.StringBuilder((int)capacity);
if (renderModels.GetComponentRenderModelName(renderModelName,componentName.ToString(), name, capacity) == 0)
continue;
var s = name.ToString();
// Only need to preloadif not already cached.
var model = models[s] as RenderModel;
if (model == null || model.mesh == null)
{
如果已经cache过了就不管了,如果没有cache过,则先把名 字记下来,后面会一一加载
renderModelNames[i]= s;
}
}
}
else
{
这里是指一个设备的渲染模型下没有子组件了,就只有一个模型(网格+ 材质),也与子组件一起统一处理
// Only need to preloadif not already cached.
var model =models[renderModelName] as RenderModel;
if (model == null || model.mesh == null)
{
尚未缓存,把模型名称加到名称数组里,下面统一处理
renderModelNames= new string[] { renderModelName };
}
else
{
已经缓存了,声明一个0元素的数组,后面会直接跳过
renderModelNames= new string[0];
}
}
// Keep trying every100ms until all components finish loading.
while (true)
{
这里是一个死循环,会把所有的模型加载进来。因此采用了异步加载模式, 所以采用了死循环协程,直到所有的模型加载完毕
var loading = false;
foreach (var name in renderModelNames)
{
if (string.IsNullOrEmpty(name))
continue;
var pRenderModel = System.IntPtr.Zero;
异步加载模型
var error =renderModels.LoadRenderModel_Async(name, ref pRenderModel);
if (error == EVRRenderModelError.Loading)
{
加载过程中
loading= true;
}
else if (error == EVRRenderModelError.None)
{
加载完成。这里会不会有点亏?加载出来的RenderModel_t中 已经带有网格数据了,这里并没有保存——应该不浪费,系统会 做缓存
// Preload textures aswell.
var renderModel = (RenderModel_t)Marshal.PtrToStructure(pRenderModel, typeof(RenderModel_t));
// Check the cache first.
var material =materials[renderModel.diffuseTextureId] as Material;
if (material == null || material.mainTexture== null)
{
材质尚未缓存,加载材质
var pDiffuseTexture =System.IntPtr.Zero;
error= renderModels.LoadTexture_Async(renderModel.diffuseTextureId, ref pDiffuseTexture);
if (error == EVRRenderModelError.Loading)
{
loading= true;
}
}
}
}
if (loading)
{
如果正在加载,则等100ms
yield return new WaitForSeconds(0.1f);
}
else
{
break;
}
}
}
模型加载完了,构建渲染模型及缓存
bool success =SetModel(renderModelName);
发出自定义模型加载完成通知
SteamVR_Utils.Event.Send("render_model_loaded", this, success);
}
构建渲染模型
private bool SetModel(string renderModelName)
{
清空网格(包括MeshRenderer及MeshFilter)
StripMesh(gameObject);
using (var holder = new RenderModelInterfaceHolder())
{
if (createComponents)
{
如果需要创建子组件,则为模型里面的子组件创建对应的子对象
if (LoadComponents(holder,renderModelName))
{
UpdateComponents();
return true;
}
加载子组件失败,并不返回,而是接着走下面的加载模型的单一网格的功 能
Debug.Log("[" + gameObject.name + "] Render model doesnot support components, falling back to single mesh.");
}
创建单一网格流程
if (!string.IsNullOrEmpty(renderModelName))
{
var model = models[renderModelName] as RenderModel;
if (model == null || model.mesh == null)
{
尚未缓存
var renderModels =holder.instance;
if (renderModels == null)
return false;
if (verbose)
Debug.Log("Loading rendermodel " + renderModelName);
果然这里又重新加载了一次,浪费了(应该也不浪费,加载过了后下 次再调用就很快了,也就是底层很有可能也有缓存)
model= LoadRenderModel(renderModels, renderModelName, renderModelName);
if (model == null)
return false;
缓存起来
models[renderModelName]= model;
}
在当前对象上添加MeshFilter和MeshRenderer
gameObject.AddComponent<MeshFilter>().mesh = model.mesh;
gameObject.AddComponent<MeshRenderer>().sharedMaterial =model.material;
return true;
}
}
return false;
}
加载指定名称的模型
RenderModel LoadRenderModel(CVRRenderModels renderModels, string renderModelName, string baseName)
{
var pRenderModel = System.IntPtr.Zero;
EVRRenderModelError error;
while ( true )
{
仍然是异步加载,不过这里实际上是同步加载的,后面直接通过Sleep等待。 但因为前面其实加载过了,这里再次调用应该会很快
error= renderModels.LoadRenderModel_Async(renderModelName, ref pRenderModel);
if (error != EVRRenderModelError.Loading)
break;
这里Sleep的是ms
System.Threading.Thread.Sleep(1);
}
if (error != EVRRenderModelError.None)
{
Debug.LogError(string.Format("Failed to loadrender model {0} - { 1}", renderModelName,error.ToString()));
return null;
}
var renderModel = (RenderModel_t)Marshal.PtrToStructure(pRenderModel, typeof(RenderModel_t));
var vertices = new Vector3[renderModel.unVertexCount];
var normals = new Vector3[renderModel.unVertexCount];
var uv = new Vector2[renderModel.unVertexCount];
返回的数据中有顶点数据,及网格三角形的顶点索引,这样就能构造出网格了
var type = typeof(RenderModel_Vertex_t);
for (int iVert = 0;iVert < renderModel.unVertexCount; iVert++)
{
var ptr = new System.IntPtr(renderModel.rVertexData.ToInt64()+ iVert * Marshal.SizeOf(type));
var vert = (RenderModel_Vertex_t)Marshal.PtrToStructure(ptr, type);
vertices[iVert] = new Vector3(vert.vPosition.v0,vert.vPosition.v1, -vert.vPosition.v2);
normals[iVert] = new Vector3(vert.vNormal.v0, vert.vNormal.v1,-vert.vNormal.v2);
uv[iVert] = new Vector2(vert.rfTextureCoord0,vert.rfTextureCoord1);
}
int indexCount = (int)renderModel.unTriangleCount * 3;
var indices = new short[indexCount];
Marshal.Copy(renderModel.rIndexData, indices, 0, indices.Length);
var triangles = new int[indexCount];
for (int iTri = 0;iTri < renderModel.unTriangleCount; iTri++)
{
triangles[iTri * 3 + 0] = (int)indices[iTri * 3 + 2];
triangles[iTri * 3 + 1] = (int)indices[iTri * 3 + 1];
triangles[iTri * 3 + 2] = (int)indices[iTri * 3 + 0];
}
转换成网格定义
var mesh = new Mesh();
mesh.vertices = vertices;
mesh.normals = normals;
mesh.uv = uv;
mesh.triangles = triangles;
网格优化,这个会耗点时间
mesh.Optimize();
//mesh.hideFlags =HideFlags.DontUnloadUnusedAsset;
// Check cache beforeloading texture.
var material =materials[renderModel.diffuseTextureId] as Material;
if (material == null || material.mainTexture == null)
{
材质没有缓存,下面也会重新加载材质
var pDiffuseTexture =System.IntPtr.Zero;
while (true)
{
error= renderModels.LoadTexture_Async(renderModel.diffuseTextureId, ref pDiffuseTexture);
if (error != EVRRenderModelError.Loading)
break;
System.Threading.Thread.Sleep(1);
}
if (error == EVRRenderModelError.None)
{
将原始数据转换成Unity纹理数据
var diffuseTexture = (RenderModel_TextureMap_t)Marshal.PtrToStructure(pDiffuseTexture, typeof(RenderModel_TextureMap_t));
var texture = new Texture2D(diffuseTexture.unWidth,diffuseTexture.unHeight, TextureFormat.ARGB32, false);
if (SystemInfo.graphicsDeviceVersion.StartsWith("OpenGL"))
{
OpenGL纹理格式
var textureMapData = new byte[diffuseTexture.unWidth *diffuseTexture.unHeight * 4]; // RGBA
Marshal.Copy(diffuseTexture.rubTextureMapData,textureMapData, 0, textureMapData.Length);
var colors = new Color32[diffuseTexture.unWidth * diffuseTexture.unHeight];
int iColor = 0;
for (int iHeight = 0; iHeight
var g =textureMapData[iColor++];
var b =textureMapData[iColor++];
var a =textureMapData[iColor++];
colors[iHeight* diffuseTexture.unWidth + iWidth] = new Color32(r, g, b, a);
}
}
texture.SetPixels32(colors);
texture.Apply();
}
else
{
DirectX纹理格式
texture.Apply();
while (true)
{
error= renderModels.LoadIntoTextureD3D11_Async(renderModel.diffuseTextureId,texture.GetNativeTexturePtr());
if (error != EVRRenderModelError.Loading)
break;
System.Threading.Thread.Sleep(1);
}
}
创建材质,使用标准shader
material= new Material(shader != null ? shader : Shader.Find("Standard"));
material.mainTexture= texture;
//material.hideFlags =HideFlags.DontUnloadUnusedAsset;
缓存
materials[renderModel.diffuseTextureId]= material;
释放
renderModels.FreeTexture(pDiffuseTexture);
}
else
{
Debug.Log("Failed to loadrender model texture for render model " + renderModelName);
}
}
// Delay freeing when we can sincewe'll often get multiple requests for the same model right
// after another (e.g.two controllers or two basestations).
释放渲染模型。在编辑器模式下,如果没有运行,则直接释放。其它情况下都通过 协程延迟释放。原因是通常会有多个模型加载请求,比如两个手柄,两个基站,那 么这两个请求的数据其实是可以共享的
#if UNITY_EDITOR
if (!Application.isPlaying)
renderModels.FreeRenderModel(pRenderModel);
else
#endif
StartCoroutine(FreeRenderModel(pRenderModel));
return new RenderModel(mesh, material);
}
IEnumerator FreeRenderModel(System.IntPtr pRenderModel)
{
1秒后再释放。通过这样的方式异步延时做某个操作也挺好的,类似于Androd里 面的SendMessageDelayed
yield return new WaitForSeconds(1.0f);
using (var holder = new RenderModelInterfaceHolder())
{
var renderModels =holder.instance;
renderModels.FreeRenderModel(pRenderModel);
}
}
根据子组件名查找子组件
public Transform FindComponent(string componentName)
{
var t = transform;
for (int i = 0;i < t.childCount; i++)
{
var child = t.GetChild(i);
if (child.name ==componentName)
return child;
}
return null;
}
销毁指定对象下的MeshRenderer和MeshFilter
private void StripMesh(GameObject go)
{
var meshRenderer = go.GetComponent<MeshRenderer>();
if (meshRenderer != null)
DestroyImmediate(meshRenderer);
var meshFilter = go.GetComponent<MeshFilter>();
if (meshFilter != null)
DestroyImmediate(meshFilter);
}
加载一个模型下的所有子组件
private bool LoadComponents(RenderModelInterfaceHolder holder, string renderModelName)
{
// Disable existing components (wewill re-enable them if referenced by this new model).
// Also strip mesh filterand renderer since these will get re-added if the new component needs them.
先禁用所有子组件(使用的是active,比enable的禁用效果更彻底)
var t = transform;
for (int i = 0;i < t.childCount; i++)
{
var child = t.GetChild(i);
child.gameObject.SetActive(false);
StripMesh(child.gameObject);
}
// If no model specified, we'redone; return success.
if (string.IsNullOrEmpty(renderModelName))
return true;
var renderModels = holder.instance;
if (renderModels == null)
return false;
逐个添加组件
var count =renderModels.GetComponentCount(renderModelName);
if (count == 0)
return false;
for (int i = 0;i < count; i++)
{
var capacity = renderModels.GetComponentName(renderModelName,(uint)i, null, 0);
if (capacity == 0)
continue;
var componentName = new System.Text.StringBuilder((int)capacity);
if (renderModels.GetComponentName(renderModelName, (uint)i, componentName, capacity) == 0)
continue;
// Create (or reuse) achild object for this component (some components are dynamic and don't havemeshes).
先看这个组件是否已经存在(注意如果一个组件如果是inactive的,通过 Unity的Find系列函数是找不到的。这里的FindComponent没有使用Unity 的Find,是自己通过遍历实现的)
t= FindComponent(componentName.ToString());
if (t != null)
{
前面设为了false,这里重新active
t.gameObject.SetActive(true);
}
else
{
添加子组件
t= new GameObject(componentName.ToString()).transform;
t.parent= transform;
t.gameObject.layer= gameObject.layer;
// Also create a child'attach' object for attaching things.
每个子组件下面都加一个“attach”对象,用于关节的关联,同时每个 子组件的位置也是通过它确定的
var attach = new GameObject(k_localTransformName).transform;
attach.parent= t;
attach.localPosition= Vector3.zero;
attach.localRotation= Quaternion.identity;
attach.localScale= Vector3.one;
attach.gameObject.layer= gameObject.layer;
}
// Reset transform.
t.localPosition= Vector3.zero;
t.localRotation = Quaternion.identity;
t.localScale = Vector3.one;
每个子组件也有一个渲染模型
capacity= renderModels.GetComponentRenderModelName(renderModelName,componentName.ToString(), null, 0);
if (capacity == 0)
continue;
var componentRenderModelName= new System.Text.StringBuilder((int)capacity);
if (renderModels.GetComponentRenderModelName(renderModelName,componentName.ToString(), componentRenderModelName, capacity) == 0)
continue;
// Check the cache orload into memory.
var model =models[componentRenderModelName] as RenderModel;
if (model == null || model.mesh == null)
{
如果渲染模型还没有缓存,则加载并加到缓存中
if (verbose)
Debug.Log("Loading rendermodel " + componentRenderModelName);
model= LoadRenderModel(renderModels, componentRenderModelName.ToString(),renderModelName);
if (model == null)
continue;
models[componentRenderModelName]= model;
}
添加网格和材质
t.gameObject.AddComponent<MeshFilter>().mesh = model.mesh;
t.gameObject.AddComponent<MeshRenderer>().sharedMaterial =model.material;
}
return true;
}
void OnEnable()
{
#if UNITY_EDITOR
没有运行的时候是不会加载模型的(没有使用override的情况下,在编辑模式下 可以显示override的模型),所以在平时编辑模式下看不到手柄的模型
if (!Application.isPlaying)
return;
#endif
if (!string.IsNullOrEmpty(modelOverride))
{
Debug.Log("Model override isreally only meant to be used in the scene view for lining things up; using itat runtime is discouraged. Use tracked device index instead to ensure thecorrect model is displayed for all users.");
这里的意思是,虽然提供了模型的override机制(通过modelOverride变 量),可以覆盖缺省的模型,但是它不能在运行时使用(从modelOverride 的使用来看,它只在非运行模式下有用到)。要通过指定正确的跟踪设备索引 来显示正确的设备模型。所以override机制的主要作用目前看来主要是可以 在编辑器中预览各种跟踪设备的模型。要想看到override的效果,可以在 [CameraRig]预制体下面找到Controller下面的Model,然后在Inspector 的SteamVR_RenderModel下面切换ModelOverride就可以了。如果看不 到,注意Model有没有禁用(通常会被禁用的)。下面是通过override查看 Oculus头显:
enabled= false;
return;
}
var system = OpenVR.System;
if (system != null && system.IsTrackedDeviceConnected((uint)index))
{
如果指定了设备索引并且设备连接上了,则(首次)更新/加载模型
UpdateModel();
}
监听一些与模型相关的事件
SteamVR_Utils.Event.Listen("device_connected", OnDeviceConnected);
SteamVR_Utils.Event.Listen("hide_render_models", OnHideRenderModels);
SteamVR_Utils.Event.Listen("ModelSkinSettingsHaveChanged", OnModelSkinSettingsHaveChanged);
}
void OnDisable()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
return;
#endif
禁用时移除相关的监听
SteamVR_Utils.Event.Remove("device_connected", OnDeviceConnected);
SteamVR_Utils.Event.Remove("hide_render_models", OnHideRenderModels);
SteamVR_Utils.Event.Remove("ModelSkinSettingsHaveChanged", OnModelSkinSettingsHaveChanged);
}
#if UNITY_EDITOR
这个记录了所有的public的变量及其值,用于判断是否有配置改变
Hashtable values;
#endif
void Update()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
只在非运行状态下判断
// See if anything haschanged since this gets called whenever anything gets touched.
使用反射获取所有public字段
var fields =GetType().GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
bool modified = false;
判断各字段有没有改变,有任何改变都重新加载
if (values == null)
{
modified= true;
}
else
{
foreach (var f in fields)
{
if (!values.Contains(f))
{
modified= true;
break;
}
var v0 = values[f];
var v1 = f.GetValue(this);
if (v1 != null)
{
if (!v1.Equals(v0))
{
modified= true;
break;
}
}
else if (v0 != null)
{
modified= true;
break;
}
}
}
if (modified)
{
只在非运行模式下才能使用override
if (renderModelName !=modelOverride)
{
renderModelName= modelOverride;
SetModel(modelOverride);
}
values= new Hashtable();
foreach (var f in fields)
values[f]= f.GetValue(this);
}
非运行模式下不动态更新组件位置。注释解释是避免OpenVR一直运行
return; // Do not updatetransforms (below) when not playing in Editor (to avoid keeping OpenVR runningall the time).
}
#endif
// Update componenttransforms dynamically.
if (updateDynamically)
动态更新组件位置
UpdateComponents();
}
更新组件位置
public void UpdateComponents()
{
var t = transform;
if (t.childCount == 0)
return;
using (var holder = new RenderModelInterfaceHolder())
{
获取当前索引的设备的(按键)状态
var controllerState = (index!= SteamVR_TrackedObject.EIndex.None) ?
SteamVR_Controller.Input((int)index).GetState() : new VRControllerState_t();
for (int i = 0; i < t.childCount; i++)
{
var child = t.GetChild(i);
var renderModels =holder.instance;
if (renderModels == null)
break;
这里是通过CVRRenderModel.GetComponentState来获取每个子组件 的状态及姿态(位置)
var componentState = new RenderModel_ComponentState_t();
if (!renderModels.GetComponentState(renderModelName, child.name, ref controllerState, ref controllerModeState, ref componentState))
continue;
子组件到整体模型(父节点)的变换矩阵,通常都是(0,0,0),从实例看, 对于手柄,trackpad/touchpad的值不是(0,0,0),但也几乎是全0了
var componentTransform = new SteamVR_Utils.RigidTransform(componentState.mTrackingToComponentRenderModel);
child.localPosition= componentTransform.pos;
child.localRotation= componentTransform.rot;
将局部相对坐标赋给了attach,这个才是真正的各子组件到父节点的偏 移
var attach =child.FindChild(k_localTransformName);
if (attach != null)
{
var attachTransform = new SteamVR_Utils.RigidTransform(componentState.mTrackingToComponentLocal);
这里使用的是世界坐标
attach.position= t.TransformPoint(attachTransform.pos);
attach.rotation= t.rotation * attachTransform.rot;
}
可见(active)状态
bool visible =(componentState.uProperties & (uint)EVRComponentProperty.IsVisible) != 0;
if (visible !=child.gameObject.activeSelf)
{
child.gameObject.SetActive(visible);
}
}
}
}
这个是由SteamVR_ControllerManager中的SetTrackedDeviceIndex中通过广播 的方式调用的,当设备连接状态发生变化的时候就会调用
public void SetDeviceIndex(int index)
{
this.index = (SteamVR_TrackedObject.EIndex)index;
设置了设备索引,就不能使用override了,而应该根据索引取实际的渲染模型
modelOverride= "";
if (enabled)
{
UpdateModel();
}
}
}
10.19. SteamVR_Skybox.cs
它有两个作用,一个是用来截图的的,它有一个定制的Inspector界面,参看SteamVR_SkyboxEditor.cs,另一个是用来在合成器中设置天空盒的,而盒成器中的天空盒是用来在进行场景切换时显示一个中间过渡界面的,参看SteamVR_LoadLevels.cs。
public class SteamVR_Skybox : MonoBehaviour
{
// Note: Unity's Left and Right Skybox shader variables areswitched.
立方体六个面上的纹理(贴图)
public Texture front, back, left, right, top,bottom;
网格大小,这只是个用于界面显示的参数,在SteamVR_SkyboxEditor里面使用,用 于将一个球体划分成cellSize大小的网格,一个网格一个网格渲染,最终生成一个完 整的截图
public enum CellSize
{
x1024, x64, x32, x16, x8
}
public CellSize StereoCellSize = CellSize.x32;
毫米为单位的瞳间距,也是用于SteamVR_SkyboxEditor中的截图参数
public float StereoIpdMm = 64.0f;
这个是由外部调用的设置天空盒六个面的纹理。唯一调用的地方是在 SteamVR_SkyboxEditor中,在截完图后,会把截好的图设到这里。当然在任何需要 的时候也可以调用,比如在SteamVR_LoadLevel的时候
public void SetTextureByIndex(int i, Texture t)
{
switch (i)
{
case 0:
front= t;
break;
case 1:
back= t;
break;
case 2:
left= t;
break;
case 3:
right= t;
break;
case 4:
top= t;
break;
case 5:
bottom= t;
break;
}
}
获取天空盒六个面的纹理
public Texture GetTextureByIndex(int i)
{
switch (i)
{
case 0:
return front;
case 1:
return back;
case 2:
return left;
case 3:
return right;
case 4:
return top;
case 5:
return bottom;
}
return null;
}
这个静态方法,可以直接往合成器设置天空盒六个面的纹理,在SteamVR_LoadLevel 当中正是直接调用了这个方法,而没有使用上面的方法
static public void SetOverride(
Texture front = null,
Texture back = null,
Texture left = null,
Texture right = null,
Texture top = null,
Texture bottom = null )
{
var compositor = OpenVR.Compositor;
if (compositor != null)
{
var handles = new Texture[] { front, back, left, right, top,bottom };
var textures = new Texture_t[6];
将Unity纹理对象转换成OpenVR的纹理对象
for (int i = 0; i < 6; i++)
{
textures[i].handle= (handles[i] != null) ? handles[i].GetNativeTexturePtr() : System.IntPtr.Zero;
textures[i].eType= SteamVR.instance.graphicsAPI;
textures[i].eColorSpace= EColorSpace.Auto;
}
设置天空盒
var error =compositor.SetSkyboxOverride(textures);
if (error != EVRCompositorError.None)
{
Debug.LogError("Failed to setskybox override with error: " + error);
if (error == EVRCompositorError.TextureIsOnWrongDevice)
Debug.Log("Set your graphicsdriver to use the same video card as the headset is plugged into forUnity.");
else if (error == EVRCompositorError.TextureUsesUnsupportedFormat)
Debug.Log("Ensure skyboxtextures are not compressed and have no mipmaps.");
}
}
}
清除天空盒(还原成默认的)
static public void ClearOverride()
{
var compositor = OpenVR.Compositor;
if (compositor != null)
compositor.ClearSkyboxOverride();
}
void OnEnable()
{
如果通过Inspector设置了天空盒,那么在Enable的时候就会设置到合成器
SetOverride(front,back, left, right, top, bottom);
}
void OnDisable()
{
ClearOverride();
}
}
10.20. SteamVR_SphericalProjection.cs
这个脚本是做球形投影的,原理不是很清楚,主要依赖于SteamVR_SphericalProjection这个Shader。在SteamVR_SkyboxEditor中有使用,调用了它的Set方法后,将相关参数传递给了Shader,然后在OnRenderImage中就会使用这个材质对图像进行修改(应该就是将图像进行球形投影的效果)。
[ExecuteInEditMode]
public class SteamVR_SphericalProjection : MonoBehaviour
{
static Material material;
public void Set(Vector3 N,
float phi0, float phi1, float theta0, float theta1, // in degrees
Vector3 uAxis, Vector3 uOrigin, float uScale,
Vector3 vAxis, Vector3 vOrigin, float vScale)
{
if (material == null)
material = new Material(Shader.Find("Custom/SteamVR_SphericalProjection"));
下面是给Shader传参数 TODO 具体这些参数的意义不明,得看懂Shader的原理
material.SetVector("_N", new Vector4(N.x, N.y, N.z));
material.SetFloat("_Phi0", phi0 * Mathf.Deg2Rad);
material.SetFloat("_Phi1", phi1 * Mathf.Deg2Rad);
material.SetFloat("_Theta0", theta0 * Mathf.Deg2Rad + Mathf.PI / 2);
material.SetFloat("_Theta1", theta1 * Mathf.Deg2Rad + Mathf.PI / 2);
material.SetVector("_UAxis", uAxis);
material.SetVector("_VAxis", vAxis);
material.SetVector("_UOrigin", uOrigin);
material.SetVector("_VOrigin", vOrigin);
material.SetFloat("_UScale", uScale);
material.SetFloat("_VScale", vScale);
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
Graphics.Blit(src, dest, material);
}
}
10.21. SteamVR_Stats.cs
用于使用GUIText显示一些HMD的统计信息。在[Status]预制体下面的_Stats子物体上有使用:
public class SteamVR_Stats : MonoBehaviour
{
相当于是静态文本。GUIText依赖于GUILayer,GUILayer又依赖于Camera。所以 GUIText绘制出来的文本只会出现在这个相机里。而从上图中可以看到,Camera的 Target Texture设置的是overlay。所以这个统计文本会以overlay的方式显示出 来
public GUIText text;
文字渐显的起始颜色(整个场景或者说当前相机)
public Color fadeColor = Color.black;
文字渐显的持续时间
public float fadeDuration = 1.0f;
void Awake()
{
if (text == null)
{
要求当前物体上有GUIText组件。可以在类声明前加上RequireComponent 属性
text= GetComponent<GUIText>();
一开始禁用
text.enabled= false;
}
if (fadeDuration > 0)
{
下面这两句是将界面从黑色渐出。只在Awake的时候有效果,没啥用
SteamVR_Fade.Start(fadeColor, 0);
SteamVR_Fade.Start(Color.clear, fadeDuration);
}
}
上次执行Update的绝对时间
double lastUpdate = 0.0f;
void Update()
{
if (text != null)
{
通过按键盘的“i”键来切换是否显示
if (Input.GetKeyDown(KeyCode.I))
{
text.enabled= !text.enabled;
}
if (text.enabled)
{
从合成器取统计信息
var compositor = OpenVR.Compositor;
if (compositor != null)
{
var timing = new Compositor_FrameTiming();
timing.m_nSize= (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(Compositor_FrameTiming));
compositor.GetFrameTiming(ref timing, 0);
只取了绝对系统时间
var update =timing.m_flSystemTimeInSeconds;
if (update > lastUpdate)
{
计算实际帧率,每调用一次Update就是一帧
var framerate = (lastUpdate> 0.0f) ? 1.0f / (update - lastUpdate): 0.0f;
lastUpdate= update;
只显示了帧率和丢帧数两个信息。其实 Compositor_FrameTiming里还是有不少其它统计信息的
text.text= string.Format("framerate: { 0:N0} ndropped frames: { 1}", framerate, (int)timing.m_nNumDroppedFrames);
}
else
{
lastUpdate= update;
}
}
}
}
}
}