//======= Copyright (c) Valve Corporation, All rights reserved. =============== // // Purpose: Render model of associated tracked object // //============================================================================= using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using Valve.VR; namespace Valve.VR { [ExecuteInEditMode] public class SteamVR_RenderModel : MonoBehaviour { public SteamVR_TrackedObject.EIndex index = SteamVR_TrackedObject.EIndex.None; protected SteamVR_Input_Sources inputSource; public const string modelOverrideWarning = "Model override is really only meant to be used in " + "the scene view for lining things up; using it at runtime is discouraged. Use tracked device " + "index instead to ensure the correct model is displayed for all users."; [Tooltip(modelOverrideWarning)] public string modelOverride; [Tooltip("Shader to apply to model.")] public Shader shader; [Tooltip("Enable to print out when render models are loaded.")] public bool verbose = false; [Tooltip("If available, break down into separate components instead of loading as a single mesh.")] public bool createComponents = true; [Tooltip("Update transforms of components at runtime to reflect user action.")] 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 component transforms at runtime. public string renderModelName { get; private set; } public bool initializedAttachPoints { get; set; } private Dictionary componentAttachPoints = new Dictionary(); private List meshRenderers = new List(); // If someone knows how to keep these from getting cleaned up every 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 and material to get them to serialize properly, // as well as tried marking the mesh and material objects as // DontUnloadUnusedAsset, but Unity was still unloading them. // The hashtable is preserving its entries, but the mesh and material // variables are going 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; } } public static Hashtable models = new Hashtable(); public static Hashtable materials = new Hashtable(); // Helper class to load render models interface on demand and clean up when done. public sealed class RenderModelInterfaceHolder : System.IDisposable { private bool needsShutdown, failedLoadInterface; private CVRRenderModels _instance; public CVRRenderModels instance { get { if (_instance == null && !failedLoadInterface) { if (Application.isEditor && Application.isPlaying == false) needsShutdown = SteamVR.InitializeTemporarySession(); _instance = OpenVR.RenderModels; if (_instance == null) { Debug.LogError("[SteamVR] Failed to load IVRRenderModels interface version " + OpenVR.IVRRenderModels_Version); failedLoadInterface = true; } } return _instance; } } public void Dispose() { if (needsShutdown) SteamVR.ExitTemporarySession(); } } private void OnModelSkinSettingsHaveChanged(VREvent_t vrEvent) { if (!string.IsNullOrEmpty(renderModelName)) { renderModelName = ""; UpdateModel(); } } public void SetMeshRendererState(bool state) { for (int rendererIndex = 0; rendererIndex < meshRenderers.Count; rendererIndex++) { MeshRenderer renderer = meshRenderers[rendererIndex]; if (renderer != null) renderer.enabled = state; } } private void OnHideRenderModels(bool hidden) { SetMeshRendererState(!hidden); } private void OnDeviceConnected(int i, bool connected) { if (i != (int)index) return; if (connected) { UpdateModel(); } } public void UpdateModel() { var system = OpenVR.System; if (system == null || index == SteamVR_TrackedObject.EIndex.None) return; var error = ETrackedPropertyError.TrackedProp_Success; var capacity = system.GetStringTrackedDeviceProperty((uint)index, ETrackedDeviceProperty.Prop_RenderModelName_String, null, 0, ref error); if (capacity <= 1) { Debug.LogError("[SteamVR] Failed to get render 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) { StartCoroutine(SetModelAsync(s)); } } IEnumerator SetModelAsync(string newRenderModelName) { meshRenderers.Clear(); if (string.IsNullOrEmpty(newRenderModelName)) yield break; // Preload all render models before asking for the data to create meshes. using (RenderModelInterfaceHolder holder = new RenderModelInterfaceHolder()) { CVRRenderModels renderModels = holder.instance; if (renderModels == null) yield break; // Gather names of render models to preload. string[] renderModelNames; uint count = renderModels.GetComponentCount(newRenderModelName); if (count > 0) { renderModelNames = new string[count]; for (int componentIndex = 0; componentIndex < count; componentIndex++) { uint capacity = renderModels.GetComponentName(newRenderModelName, (uint)componentIndex, null, 0); if (capacity == 0) continue; var componentNameStringBuilder = new System.Text.StringBuilder((int)capacity); if (renderModels.GetComponentName(newRenderModelName, (uint)componentIndex, componentNameStringBuilder, capacity) == 0) continue; string componentName = componentNameStringBuilder.ToString(); capacity = renderModels.GetComponentRenderModelName(newRenderModelName, componentName, null, 0); if (capacity == 0) continue; var nameStringBuilder = new System.Text.StringBuilder((int)capacity); if (renderModels.GetComponentRenderModelName(newRenderModelName, componentName, nameStringBuilder, capacity) == 0) continue; var s = nameStringBuilder.ToString(); // Only need to preload if not already cached. RenderModel model = models[s] as RenderModel; if (model == null || model.mesh == null) { renderModelNames[componentIndex] = s; } } } else { // Only need to preload if not already cached. RenderModel model = models[newRenderModelName] as RenderModel; if (model == null || model.mesh == null) { renderModelNames = new string[] { newRenderModelName }; } else { renderModelNames = new string[0]; } } // Keep trying every 100ms until all components finish loading. while (true) { var loading = false; for (int renderModelNameIndex = 0; renderModelNameIndex < renderModelNames.Length; renderModelNameIndex++) { if (string.IsNullOrEmpty(renderModelNames[renderModelNameIndex])) continue; var pRenderModel = System.IntPtr.Zero; var error = renderModels.LoadRenderModel_Async(renderModelNames[renderModelNameIndex], ref pRenderModel); //Debug.Log("[SteamVR] renderModels.LoadRenderModel_Async(" + renderModelNames[renderModelNameIndex] + ": " + error.ToString()); if (error == EVRRenderModelError.Loading) { loading = true; } else if (error == EVRRenderModelError.None) { // Preload textures as well. var renderModel = MarshalRenderModel(pRenderModel); // 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); //Debug.Log("[SteamVR] renderModels.LoadRenderModel_Async(" + renderModelNames[renderModelNameIndex] + ": " + error.ToString()); if (error == EVRRenderModelError.Loading) { loading = true; } } } } if (loading) { yield return new WaitForSecondsRealtime(0.1f); } else { break; } } } bool success = SetModel(newRenderModelName); this.renderModelName = newRenderModelName; SteamVR_Events.RenderModelLoaded.Send(this, success); } private bool SetModel(string renderModelName) { StripMesh(gameObject); using (var holder = new RenderModelInterfaceHolder()) { if (createComponents) { componentAttachPoints.Clear(); if (LoadComponents(holder, renderModelName)) { UpdateComponents(holder.instance); return true; } Debug.Log("[SteamVR] [" + gameObject.name + "] Render model does not 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("[SteamVR] Loading render model " + renderModelName); model = LoadRenderModel(renderModels, renderModelName, renderModelName); if (model == null) return false; models[renderModelName] = model; } gameObject.AddComponent().mesh = model.mesh; MeshRenderer newRenderer = gameObject.AddComponent(); newRenderer.sharedMaterial = model.material; meshRenderers.Add(newRenderer); return true; } } return false; } RenderModel LoadRenderModel(CVRRenderModels renderModels, string renderModelName, string baseName) { var pRenderModel = System.IntPtr.Zero; EVRRenderModelError error; while (true) { error = renderModels.LoadRenderModel_Async(renderModelName, ref pRenderModel); if (error != EVRRenderModelError.Loading) break; Sleep(); } if (error != EVRRenderModelError.None) { Debug.LogError(string.Format("[SteamVR] Failed to load render model {0} - {1}", renderModelName, error.ToString())); return null; } var renderModel = MarshalRenderModel(pRenderModel); 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; #if (UNITY_5_4 || UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0) mesh.Optimize(); #endif //mesh.hideFlags = HideFlags.DontUnloadUnusedAsset; // Check cache before loading 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; Sleep(); } if (error == EVRRenderModelError.None) { var diffuseTexture = MarshalRenderModel_TextureMap(pDiffuseTexture); var texture = new Texture2D(diffuseTexture.unWidth, diffuseTexture.unHeight, TextureFormat.RGBA32, false); if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Direct3D11) { texture.Apply(); System.IntPtr texturePointer = texture.GetNativeTexturePtr(); while (true) { error = renderModels.LoadIntoTextureD3D11_Async(renderModel.diffuseTextureId, texturePointer); if (error != EVRRenderModelError.Loading) break; Sleep(); } } else { 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 < diffuseTexture.unHeight; iHeight++) { for (int iWidth = 0; iWidth < diffuseTexture.unWidth; iWidth++) { var r = textureMapData[iColor++]; 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(); } #if UNITY_URP material = new Material(shader != null ? shader : Shader.Find("Universal Render Pipeline/Lit")); #else material = new Material(shader != null ? shader : Shader.Find("Standard")); #endif material.mainTexture = texture; //material.hideFlags = HideFlags.DontUnloadUnusedAsset; materials[renderModel.diffuseTextureId] = material; renderModels.FreeTexture(pDiffuseTexture); } else { Debug.Log("[SteamVR] Failed to load render model texture for render model " + renderModelName + ". Error: " + error.ToString()); } } // Delay freeing when we can since we'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) { yield return new WaitForSeconds(1.0f); using (var holder = new RenderModelInterfaceHolder()) { var renderModels = holder.instance; renderModels.FreeRenderModel(pRenderModel); } } public Transform FindTransformByName(string componentName, Transform inTransform = null) { if (inTransform == null) inTransform = this.transform; for (int childIndex = 0; childIndex < inTransform.childCount; childIndex++) { Transform child = inTransform.GetChild(childIndex); if (child.name == componentName) return child; } return null; } public Transform GetComponentTransform(string componentName) { if (componentName == null) return this.transform; if (componentAttachPoints.ContainsKey(componentName)) return componentAttachPoints[componentName]; return null; } private void StripMesh(GameObject go) { var meshRenderer = go.GetComponent(); if (meshRenderer != null) DestroyImmediate(meshRenderer); var meshFilter = go.GetComponent(); if (meshFilter != null) DestroyImmediate(meshFilter); } private bool LoadComponents(RenderModelInterfaceHolder holder, string renderModelName) { // Disable existing components (we will re-enable them if referenced by this new model). // Also strip mesh filter and renderer since these will get re-added if the new component needs them. var t = transform; for (int childIndex = 0; childIndex < t.childCount; childIndex++) { var child = t.GetChild(childIndex); child.gameObject.SetActive(false); StripMesh(child.gameObject); } // If no model specified, we're done; 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; System.Text.StringBuilder componentNameStringBuilder = new System.Text.StringBuilder((int)capacity); if (renderModels.GetComponentName(renderModelName, (uint)i, componentNameStringBuilder, capacity) == 0) continue; string componentName = componentNameStringBuilder.ToString(); // Create (or reuse) a child object for this component (some components are dynamic and don't have meshes). t = FindTransformByName(componentName); if (t != null) { t.gameObject.SetActive(true); componentAttachPoints[componentName] = FindTransformByName(k_localTransformName, t); } else { t = new GameObject(componentName).transform; t.parent = transform; t.gameObject.layer = gameObject.layer; // Also create a child 'attach' object for attaching things. 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; componentAttachPoints[componentName] = attach; } // Reset transform. t.localPosition = Vector3.zero; t.localRotation = Quaternion.identity; t.localScale = Vector3.one; capacity = renderModels.GetComponentRenderModelName(renderModelName, componentName, null, 0); if (capacity == 0) continue; var componentRenderModelNameStringBuilder = new System.Text.StringBuilder((int)capacity); if (renderModels.GetComponentRenderModelName(renderModelName, componentName, componentRenderModelNameStringBuilder, capacity) == 0) continue; string componentRenderModelName = componentRenderModelNameStringBuilder.ToString(); // Check the cache or load into memory. var model = models[componentRenderModelName] as RenderModel; if (model == null || model.mesh == null) { if (verbose) Debug.Log("[SteamVR] Loading render model " + componentRenderModelName); model = LoadRenderModel(renderModels, componentRenderModelName, renderModelName); if (model == null) continue; models[componentRenderModelName] = model; } t.gameObject.AddComponent().mesh = model.mesh; MeshRenderer newRenderer = t.gameObject.AddComponent(); newRenderer.sharedMaterial = model.material; meshRenderers.Add(newRenderer); } return true; } SteamVR_Events.Action deviceConnectedAction, hideRenderModelsAction, modelSkinSettingsHaveChangedAction; SteamVR_RenderModel() { deviceConnectedAction = SteamVR_Events.DeviceConnectedAction(OnDeviceConnected); hideRenderModelsAction = SteamVR_Events.HideRenderModelsAction(OnHideRenderModels); modelSkinSettingsHaveChangedAction = SteamVR_Events.SystemAction(EVREventType.VREvent_ModelSkinSettingsHaveChanged, OnModelSkinSettingsHaveChanged); } void OnEnable() { #if UNITY_EDITOR if (!Application.isPlaying) return; #endif if (!string.IsNullOrEmpty(modelOverride)) { Debug.Log("[SteamVR] " + modelOverrideWarning); enabled = false; return; } var system = OpenVR.System; if (system != null && system.IsTrackedDeviceConnected((uint)index)) { UpdateModel(); } deviceConnectedAction.enabled = true; hideRenderModelsAction.enabled = true; modelSkinSettingsHaveChangedAction.enabled = true; } void OnDisable() { #if UNITY_EDITOR if (!Application.isPlaying) return; #endif deviceConnectedAction.enabled = false; hideRenderModelsAction.enabled = false; modelSkinSettingsHaveChangedAction.enabled = false; } #if UNITY_EDITOR Hashtable values; #endif void Update() { #if UNITY_EDITOR if (!Application.isPlaying) { // See if anything has changed since this gets called whenever anything gets touched. 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) { if (renderModelName != modelOverride) { renderModelName = modelOverride; SetModel(modelOverride); } values = new Hashtable(); foreach (var f in fields) values[f] = f.GetValue(this); } return; // Do not update transforms (below) when not playing in Editor (to avoid keeping OpenVR running all the time). } #endif // Update component transforms dynamically. if (updateDynamically) UpdateComponents(OpenVR.RenderModels); } Dictionary nameCache; public void UpdateComponents(CVRRenderModels renderModels) { if (renderModels == null) return; if (transform.childCount == 0) return; if (nameCache == null) nameCache = new Dictionary(); for (int childIndex = 0; childIndex < transform.childCount; childIndex++) { Transform child = transform.GetChild(childIndex); // Cache names since accessing an object's name allocates memory. string componentName; if (!nameCache.TryGetValue(child.GetInstanceID(), out componentName)) { componentName = child.name; nameCache.Add(child.GetInstanceID(), componentName); } var componentState = new RenderModel_ComponentState_t(); if (!renderModels.GetComponentStateForDevicePath(renderModelName, componentName, SteamVR_Input_Source.GetHandle(inputSource), ref controllerModeState, ref componentState)) continue; child.localPosition = componentState.mTrackingToComponentRenderModel.GetPosition(); child.localRotation = componentState.mTrackingToComponentRenderModel.GetRotation(); Transform attach = null; for (int childChildIndex = 0; childChildIndex < child.childCount; childChildIndex++) { Transform childChild = child.GetChild(childChildIndex); int childInstanceID = childChild.GetInstanceID(); string childName; if (!nameCache.TryGetValue(childInstanceID, out childName)) { childName = childChild.name; nameCache.Add(childInstanceID, componentName); } if (childName == SteamVR_RenderModel.k_localTransformName) attach = childChild; } if (attach != null) { attach.position = transform.TransformPoint(componentState.mTrackingToComponentLocal.GetPosition()); attach.rotation = transform.rotation * componentState.mTrackingToComponentLocal.GetRotation(); initializedAttachPoints = true; } bool visible = (componentState.uProperties & (uint)EVRComponentProperty.IsVisible) != 0; if (visible != child.gameObject.activeSelf) { child.gameObject.SetActive(visible); } } } public void SetDeviceIndex(int newIndex) { this.index = (SteamVR_TrackedObject.EIndex)newIndex; modelOverride = ""; if (enabled) { UpdateModel(); } } public void SetInputSource(SteamVR_Input_Sources newInputSource) { inputSource = newInputSource; } private static void Sleep() { #if !UNITY_METRO //System.Threading.Thread.SpinWait(1); //faster napping System.Threading.Thread.Sleep(1); #endif } /// /// Helper function to handle the inconvenient fact that the packing for RenderModel_t is /// different on Linux/OSX (4) than it is on Windows (8) /// /// native pointer to the RenderModel_t /// private RenderModel_t MarshalRenderModel(System.IntPtr pRenderModel) { if ((System.Environment.OSVersion.Platform == System.PlatformID.MacOSX) || (System.Environment.OSVersion.Platform == System.PlatformID.Unix)) { var packedModel = (RenderModel_t_Packed)Marshal.PtrToStructure(pRenderModel, typeof(RenderModel_t_Packed)); RenderModel_t model = new RenderModel_t(); packedModel.Unpack(ref model); return model; } else { return (RenderModel_t)Marshal.PtrToStructure(pRenderModel, typeof(RenderModel_t)); } } /// /// Helper function to handle the inconvenient fact that the packing for RenderModel_TextureMap_t is /// different on Linux/OSX (4) than it is on Windows (8) /// /// native pointer to the RenderModel_TextureMap_t /// private RenderModel_TextureMap_t MarshalRenderModel_TextureMap(System.IntPtr pRenderModel) { if ((System.Environment.OSVersion.Platform == System.PlatformID.MacOSX) || (System.Environment.OSVersion.Platform == System.PlatformID.Unix)) { var packedModel = (RenderModel_TextureMap_t_Packed)Marshal.PtrToStructure(pRenderModel, typeof(RenderModel_TextureMap_t_Packed)); RenderModel_TextureMap_t model = new RenderModel_TextureMap_t(); packedModel.Unpack(ref model); return model; } else { return (RenderModel_TextureMap_t)Marshal.PtrToStructure(pRenderModel, typeof(RenderModel_TextureMap_t)); } } } }