/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections; using System.Collections.Generic; using System.IO; using System; using System.Linq; using UnityEngine; using OVRSimpleJSON; using System.Threading.Tasks; public enum OVRChunkType { JSON = 0x4E4F534A, BIN = 0x004E4942, } public enum OVRTextureFormat { NONE, KTX2, PNG, JPEG, } /// /// This enum represents a simplified representation on how Texture Filter quality is implemented in Unity. /// The values set in this enum are NOT random and are directly used by ApplyTextureQuality() and DetectTextureQuality() /// to get/set the correspondent setup in Unity. /// public enum OVRTextureQualityFiltering { None = -1, Bilinear = 0, Trilinear = 1, Aniso2x = 2, Aniso4x = 3, Aniso8x = 4, Aniso16x = 5, } /// /// Struct that contains mesh data loaded from a GLTF (GL Transmission Format file). This includes the mesh object, material of the mesh, mesh attributes, and the morph targets of the mesh. /// public struct OVRMeshData { public Mesh mesh; public Material material; public OVRMeshAttributes baseAttributes; public OVRMeshAttributes[] morphTargets; } /// /// Struct that contains material data from a GLTF (GL Transmission Format file) required for creating a Unity material. This includes the shader, texture data, and base color. /// public struct OVRMaterialData { public Shader shader; public int textureId; public OVRTextureData texture; public Color baseColorFactor; } /// /// Struct that contains the overall GLTF (GL Transmission Format) scene structure. This includes the root of the scene as a game object as well as all the animation and morph target data for the scene. /// public struct OVRGLTFScene { public GameObject root; public Dictionary animationNodes; public Dictionary animationNodeLookup; public List morphTargetHandlers; } /// /// Struct that contains data on a texture loaded from a GLTF (GL Transmission Format file). This includes the raw data of the texture as well as metadata like width, height, format, and uri. /// public struct OVRTextureData { public byte[] data; public int width; public int height; public OVRTextureFormat format; public TextureFormat transcodedFormat; public string uri; } /// /// Struct that contains the mesh attribute data for each mesh loaded from a GLTF (GL Transmission Format file). This includes the verts, normals, tangets, UVs, colors, and bone weights. /// public struct OVRMeshAttributes { public Vector3[] vertices; public Vector3[] normals; public Vector4[] tangents; public Vector2[] texcoords; public Color[] colors; public BoneWeight[] boneWeights; } /// /// This is a lightweight [GLTF model](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) loader that is guaranteed to work with models loaded from the Meta Quest runtime /// using OVRPlugin.LoadRenderModel . The loader only accepts GLTF files/data in the [GLB format](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#glb-file-format-specification). /// We do not recommended using this class as a general purpose GLTF loader. /// public class OVRGLTFLoader { private const float LoadingMaxTimePerFrame = 1.0f / 70f; private readonly Func m_deferredStream; private JSONNode m_jsonData; private Stream m_glbStream; private GameObject[] m_Nodes; private Dictionary m_InputAnimationNodes; // private Dictionary m_AnimationLookup; // private Dictionary m_morphTargetHandlers; private Shader m_Shader = Shader.Find("Legacy Shaders/Diffuse"); private Shader m_AlphaBlendShader = Shader.Find("Unlit/Transparent"); private OVRTextureQualityFiltering m_TextureQuality = OVRTextureQualityFiltering.Bilinear; // = Unity default private float m_TextureMipmapBias = 0.0f; // = shader default public OVRGLTFScene scene; public static readonly Vector3 GLTFToUnitySpace = new Vector3(-1, 1, 1); public static readonly Vector3 GLTFToUnityTangent = new Vector4(-1, 1, 1, -1); public static readonly Vector4 GLTFToUnitySpace_Rotation = new Vector4(1, -1, -1, 1); private static Dictionary InputNodeNameMap = new Dictionary { { "button_a", OVRGLTFInputNode.Button_A_X }, { "button_x", OVRGLTFInputNode.Button_A_X }, { "button_b", OVRGLTFInputNode.Button_B_Y }, { "button_y", OVRGLTFInputNode.Button_B_Y }, { "button_oculus", OVRGLTFInputNode.Button_Oculus_Menu }, { "trigger_front", OVRGLTFInputNode.Trigger_Front }, { "trigger_grip", OVRGLTFInputNode.Trigger_Grip }, { "thumbstick", OVRGLTFInputNode.ThumbStick }, }; public Func textureUriHandler; private Dictionary m_textures; private Dictionary m_materials; private float m_processingNodesStart; private OVRGLTFAccessor _dataAccessor; /// /// Creates a new GLTF loader using a file path. /// /// Path to GLB file public OVRGLTFLoader(string fileName) { m_glbStream = File.Open(fileName, FileMode.Open); } /// /// Creates a new GLTF loader using a binary data array. /// /// GLB byte data public OVRGLTFLoader(byte[] data) { m_glbStream = new MemoryStream(data, 0, data.Length, false, true); } /// /// Creates a new GLTF loader with a deferred data stream. This should be used with to allow for async loading of the GLTF model. This can help prevent stalling on complex GLTF models. /// /// GLB data as a Stream public OVRGLTFLoader(Func deferredStream) { m_deferredStream = deferredStream; } /// /// Starts the loading process for the full GLTF file into Unity. /// /// Load GLTF animations. /// Load GLTF textures with mip maps. /// Full GLTF scene public OVRGLTFScene LoadGLB(bool supportAnimation, bool loadMips = true) { var loadGltfCoroutine = LoadGLBCoroutine(supportAnimation, loadMips); while (loadGltfCoroutine.MoveNext()) { // process the coroutine synchronously } return scene; } /// /// Starts the loading process for the full GLTF file into Unity as a coroutine. This can help prevent stalling as loading will be spread out over multiple frames. /// /// Load GLTF animations /// Load GLTF textures with mip maps. /// IEnumerator that can be yielded on. public IEnumerator LoadGLBCoroutine(bool supportAnimation, bool loadMips = true) { scene = new OVRGLTFScene(); m_InputAnimationNodes = new Dictionary(); m_AnimationLookup = new Dictionary(); m_morphTargetHandlers = new Dictionary(); m_textures = new Dictionary(); m_materials = new Dictionary(); // If running in the unity editor avoid a background task if (Application.isBatchMode) { Debug.Log("Batch Mode Single Threaded Loading"); m_jsonData = InitializeGLBLoad(); } else { var task = Task.Run(() => InitializeGLBLoad()); yield return new WaitUntil(() => task.IsCompleted); m_jsonData = task.Result; if (task.IsFaulted) { Debug.LogException(task.Exception); } } if (m_jsonData == null || !OVRGLTFAccessor.TryCreate(m_jsonData["accessors"], m_jsonData["bufferViews"], m_jsonData["buffers"], m_glbStream, out _dataAccessor)) { m_glbStream?.Close(); yield break; } var loadGltf = LoadGLTF(supportAnimation, loadMips); // Run coroutine withut initial frame skip while (loadGltf.MoveNext()) { yield return loadGltf.Current; } m_glbStream.Close(); if (!m_Nodes.Any()) { yield break; } // Rotate to match unity coordinates scene.root.transform.Rotate(Vector3.up, 180.0f); scene.root.SetActive(true); scene.animationNodes = m_InputAnimationNodes; scene.animationNodeLookup = m_AnimationLookup; scene.morphTargetHandlers = m_morphTargetHandlers.Values.ToList(); } private JSONNode InitializeGLBLoad() { if (m_deferredStream != null) { m_glbStream = m_deferredStream.Invoke(); } if (ValidateGLB(m_glbStream)) { byte[] jsonChunkData = ReadChunk(m_glbStream, OVRChunkType.JSON); if (jsonChunkData != null) { string json = System.Text.Encoding.ASCII.GetString(jsonChunkData); return JSON.Parse(json); } } return null; } /// /// Sets the shader that should be used by the GLTF loader on materials. /// /// Shader to be used. public void SetModelShader(Shader shader) { m_Shader = shader; } /// /// Sets the alpha blending shader that should be used by the GLTF loader on materials. /// /// Shader to be used. public void SetModelAlphaBlendShader(Shader shader) { m_AlphaBlendShader = shader; } /// /// Sets the texture quality for loading all textures in the GLTF model. The default is Bilinear. /// Once loaded, textures will be read-only on GPU memory. /// /// The quality setting. public void SetTextureQualityFiltering(OVRTextureQualityFiltering loadedTexturesQuality) { m_TextureQuality = loadedTexturesQuality; } /// /// Sets the MipMap bias value for loading all textures in the GLTF model. The default is 0. /// Only supported when MipMaps are loaded and the provided shader has a property named "_MainTexMMBias" /// /// The value for bias. Value is clamped between [-1,1] public void SetMipMapBias(float loadedTexturesMipmapBiasing) { m_TextureMipmapBias = Mathf.Clamp(loadedTexturesMipmapBiasing, -1.0f, 1.0f); } /// /// Decodes the Texture Quality setting from the input Texture2D properties' values. /// /// The input Texture2D /// The enum TextureQualityFiltering representing the quality. public static OVRTextureQualityFiltering DetectTextureQuality(in Texture2D srcTexture) { OVRTextureQualityFiltering quality = OVRTextureQualityFiltering.None; switch (srcTexture.filterMode) { case FilterMode.Point: quality = OVRTextureQualityFiltering.None; break; case FilterMode.Bilinear: goto default; case FilterMode.Trilinear: if (srcTexture.anisoLevel <= 1) quality = OVRTextureQualityFiltering.Trilinear; // In theory, aniso supports values between 2-16x, but in reality GPUs and gfx APIs implement // powers of 2 (values in between have no change) else if (srcTexture.anisoLevel < 4) quality = OVRTextureQualityFiltering.Aniso2x; else if (srcTexture.anisoLevel < 8) quality = OVRTextureQualityFiltering.Aniso4x; else if (srcTexture.anisoLevel < 16) quality = OVRTextureQualityFiltering.Aniso8x; else quality = OVRTextureQualityFiltering.Aniso16x; break; default: quality = OVRTextureQualityFiltering.Bilinear; break; } return quality; } /// /// Applies the input Texture Quality setting into the ref Texture2D provided as input. Texture2D must not be readonly. /// /// The quality level to apply /// The destination Texture2D to apply quality setting to public static void ApplyTextureQuality(OVRTextureQualityFiltering qualityLevel, ref Texture2D destTexture) { if (destTexture == null) return; switch (qualityLevel) { case OVRTextureQualityFiltering.None: destTexture.filterMode = FilterMode.Point; destTexture.anisoLevel = 0; break; case OVRTextureQualityFiltering.Bilinear: destTexture.filterMode = FilterMode.Bilinear; destTexture.anisoLevel = 0; break; case OVRTextureQualityFiltering.Trilinear: destTexture.filterMode = FilterMode.Trilinear; destTexture.anisoLevel = 0; break; default: // for higher values destTexture.filterMode = FilterMode.Trilinear; // In theory, aniso supports values between 2-16x, but in reality GPUs and gfx APIs implement // powers of 2 (values in between have no change) // given the enum value, this gives aniso x2 x4 x8 x16 destTexture.anisoLevel = Mathf.FloorToInt(Mathf.Pow(2.0f, (int)qualityLevel - 1)); break; } } /// /// Checks the GLB data stream to check if it is a valid GLTF file. /// /// Data stream of the GLB file. /// If the stream is a valid GLTF file. static public bool ValidateGLB(Stream glbStream) { if (glbStream == null) { return false; } // Read the magic entry and ensure value matches the glTF value int uint32Size = sizeof(uint); byte[] buffer = new byte[uint32Size]; glbStream.Read(buffer, 0, uint32Size); uint magic = BitConverter.ToUInt32(buffer, 0); if (magic != 0x46546C67) { Debug.LogError("Data stream was not a valid glTF format"); return false; } // Read glTF version glbStream.Read(buffer, 0, uint32Size); uint version = BitConverter.ToUInt32(buffer, 0); if (version != 2) { Debug.LogError("Only glTF 2.0 is supported"); return false; } // Read glTF file size glbStream.Read(buffer, 0, uint32Size); uint length = BitConverter.ToUInt32(buffer, 0); if (length != glbStream.Length) { Debug.LogError("glTF header length does not match file length"); return false; } return true; } /// /// Reads a chunk of binary data from the stream that either represents the JSON or BIN part of the GLTF file. /// /// The binary stream of the GLTF file. /// Type indicating which part of the GLTF file should be read. /// public static byte[] ReadChunk(Stream glbStream, OVRChunkType type) { uint chunkLength; if (ValidateChunk(glbStream, type, out chunkLength)) { byte[] chunkBuffer = new byte[chunkLength]; glbStream.Read(chunkBuffer, 0, (int)chunkLength); return chunkBuffer; } return null; } private static bool ValidateChunk(Stream glbStream, OVRChunkType type, out uint chunkLength) { int uint32Size = sizeof(uint); byte[] buffer = new byte[uint32Size]; glbStream.Read(buffer, 0, uint32Size); chunkLength = BitConverter.ToUInt32(buffer, 0); glbStream.Read(buffer, 0, uint32Size); uint chunkType = BitConverter.ToUInt32(buffer, 0); if (chunkType != (uint)type) { Debug.LogError("Read chunk does not match type."); return false; } return true; } private IEnumerator LoadGLTF(bool supportAnimation, bool loadMips) { if (m_jsonData == null) { Debug.LogError("m_jsonData was null"); yield break; } var scenes = m_jsonData["scenes"]; if (scenes.Count == 0) { Debug.LogError("No valid scenes in this glTF."); yield break; } // Create GameObjects for each node in the model so that they can be referenced during processing scene.root = new GameObject("GLB Scene Root"); var sceneRootTransform = scene.root.transform; scene.root.SetActive(false); var nodes = m_jsonData["nodes"].AsArray; m_Nodes = new GameObject[nodes.Count]; sceneRootTransform.hierarchyCapacity = nodes.Count; var i = 0; foreach (var node in nodes.Values) { var go = new GameObject(); go.transform.SetParent(sceneRootTransform, false); m_Nodes[i++] = go; } // Limit loading to just the first scene in the glTF var mainScene = scenes[0]; var rootNodes = mainScene["nodes"].AsArray; m_processingNodesStart = Time.realtimeSinceStartup; // Load all nodes (some models like e.g. laptops use multiple nodes) foreach (JSONNode rootNode in rootNodes) { int rootNodeId = rootNode.AsInt; var processNode = ProcessNode(nodes, rootNodeId, loadMips, sceneRootTransform); // Run coroutine without initial frame skip while (processNode.MoveNext()) { yield return processNode.Current; } } if (supportAnimation) { var processAnimations = ProcessAnimations(); // Run coroutine without initial frame skip while (processAnimations.MoveNext()) { yield return processAnimations.Current; } } } private IEnumerator ProcessNode(JSONArray nodes, int nodeId, bool loadMips, Transform parent) { bool hasSkipped = false; if (Time.realtimeSinceStartup - m_processingNodesStart > LoadingMaxTimePerFrame) { m_processingNodesStart = Time.realtimeSinceStartup; hasSkipped = true; yield return null; } JSONNode node = nodes[nodeId]; var nodeGameObject = m_Nodes[nodeId]; var nodeTransform = nodeGameObject.transform; var nodeName = node["name"].Value; nodeTransform.name = nodeName; nodeTransform.SetParent(parent, false); // Process the child nodes first var childNodes = node["children"].AsArray; if (childNodes.Count > 0) { foreach (var child in childNodes.Values) { var childId = child.AsInt; var processNode = ProcessNode(nodes, childId, loadMips, nodeTransform); // Run coroutine without initial frame skip while (processNode.MoveNext()) { yield return processNode.Current; } } } if (nodeName.StartsWith("batteryIndicator")) { nodeGameObject.SetActive(false); yield break; } if (node["mesh"] != null) { var meshId = node["mesh"].AsInt; OVRMeshData meshData = ProcessMesh(m_jsonData["meshes"][meshId], loadMips); if (node["skin"] != null) { var renderer = nodeGameObject.AddComponent(); renderer.sharedMesh = meshData.mesh; renderer.sharedMaterial = meshData.material; var skinId = node["skin"].AsInt; ProcessSkin(m_jsonData["skins"][skinId], renderer); } else { var filter = nodeGameObject.AddComponent(); filter.sharedMesh = meshData.mesh; var renderer = nodeGameObject.AddComponent(); renderer.sharedMaterial = meshData.material; } if (meshData.morphTargets != null) { m_morphTargetHandlers[nodeId] = new OVRGLTFAnimationNodeMorphTargetHandler(meshData); } } var translation = node["translation"].AsArray; var rotation = node["rotation"].AsArray; var scale = node["scale"].AsArray; if (translation.Count > 0 || rotation.Count > 0) { var pos = Vector3.zero; var rot = Quaternion.identity; if (translation.Count > 0) { pos = new Vector3( translation[0] * GLTFToUnitySpace.x, translation[1] * GLTFToUnitySpace.y, translation[2] * GLTFToUnitySpace.z); } if (rotation.Count > 0) { rot = new Quaternion( rotation[0] * GLTFToUnitySpace.x * -1.0f, rotation[1] * GLTFToUnitySpace.y * -1.0f, rotation[2] * GLTFToUnitySpace.z * -1.0f, rotation[3] ); } nodeTransform.SetPositionAndRotation(pos, rot); } if (scale.Count > 0) { nodeTransform.localScale = new Vector3(scale[0], scale[1], scale[2]); // disable any zero-scale gameobjects to reduce drawcalls nodeTransform.gameObject.SetActive(nodeTransform.gameObject.transform.localScale != Vector3.zero); } var delta = Time.realtimeSinceStartup - m_processingNodesStart; if (!hasSkipped && Time.realtimeSinceStartup - m_processingNodesStart > LoadingMaxTimePerFrame) { m_processingNodesStart = Time.realtimeSinceStartup; yield return null; } } private OVRMeshData ProcessMesh(JSONNode meshNode, bool loadMips) { OVRMeshData meshData = new OVRMeshData(); int totalVertexCount = 0; var primitives = meshNode["primitives"]; int[] primitiveVertexCounts = new int[primitives.Count]; for (int i = 0; i < primitives.Count; i++) { var jsonPrimitive = primitives[i]; var jsonAttrbite = jsonPrimitive["attributes"]["POSITION"]; var jsonAccessor = m_jsonData["accessors"][jsonAttrbite.AsInt]; primitiveVertexCounts[i] = jsonAccessor["count"]; totalVertexCount += primitiveVertexCounts[i]; } int[][] indicies = new int[primitives.Count][]; // Begin async processing of material and its texture var jsonMaterial = primitives[0]["material"]; if (jsonMaterial != null) { var matData = ProcessMaterial(jsonMaterial.AsInt); matData.texture = ProcessTexture(matData.textureId); TranscodeTexture(ref matData.texture); // reuse materials whenever possible int matId = jsonMaterial.AsInt; if (m_materials.TryGetValue(matId, out var cachedMat)) { meshData.material = cachedMat; } else { Material mat = CreateUnityMaterial(matData, loadMips); m_materials.Add(matId, mat); meshData.material = mat; } } OVRMeshAttributes attributes = new OVRMeshAttributes(); OVRMeshAttributes[] morphTargetAttributes = null; int vertexOffset = 0; for (int i = 0; i < primitives.Count; i++) { var jsonPrimitive = primitives[i]; int indicesAccessorId = jsonPrimitive["indices"].AsInt; _dataAccessor.Seek(indicesAccessorId); indicies[i] = _dataAccessor.ReadInt(); FlipTriangleIndices(ref indicies[i]); attributes = ReadMeshAttributes(jsonPrimitive["attributes"], totalVertexCount, vertexOffset); // morph targets var jsonAttribute = jsonPrimitive["targets"]; if (jsonAttribute != null) { morphTargetAttributes = new OVRMeshAttributes[jsonAttribute.Count]; for (var ii = 0; ii < jsonAttribute.Count; ii++) { morphTargetAttributes[ii] = ReadMeshAttributes(jsonAttribute[ii], totalVertexCount, vertexOffset); } } vertexOffset += primitiveVertexCounts[i]; } Mesh mesh = new Mesh(); mesh.vertices = attributes.vertices; mesh.normals = attributes.normals; mesh.tangents = attributes.tangents; mesh.colors = attributes.colors; mesh.uv = attributes.texcoords; mesh.boneWeights = attributes.boneWeights; mesh.subMeshCount = primitives.Count; int baseVertex = 0; for (int i = 0; i < primitives.Count; i++) { mesh.SetIndices(indicies[i], MeshTopology.Triangles, i, false, baseVertex); baseVertex += primitiveVertexCounts[i]; } mesh.RecalculateBounds(); meshData.mesh = mesh; meshData.morphTargets = morphTargetAttributes; if (morphTargetAttributes != null) { meshData.baseAttributes = attributes; } return meshData; } private static void FlipTriangleIndices(ref int[] indices) { for (var i = 0; i < indices.Length; i += 3) { (indices[i], indices[i + 2]) = (indices[i + 2], indices[i]); } } private OVRMeshAttributes ReadMeshAttributes(JSONNode jsonAttributes, int totalVertexCount, int vertexOffset) { OVRMeshAttributes results = new OVRMeshAttributes(); var jsonAttribute = jsonAttributes["POSITION"]; if (jsonAttribute != null) { _dataAccessor.Seek(jsonAttribute.AsInt); results.vertices = _dataAccessor.ReadVector3(GLTFToUnitySpace); } jsonAttribute = jsonAttributes["NORMAL"]; if (jsonAttribute != null) { _dataAccessor.Seek(jsonAttribute.AsInt); results.normals = _dataAccessor.ReadVector3(GLTFToUnitySpace); } jsonAttribute = jsonAttributes["TANGENT"]; if (jsonAttribute != null) { _dataAccessor.Seek(jsonAttribute.AsInt); results.tangents = _dataAccessor.ReadVector4(GLTFToUnityTangent); } jsonAttribute = jsonAttributes["TEXCOORD_0"]; if (jsonAttribute != null) { _dataAccessor.Seek(jsonAttribute.AsInt); results.texcoords = _dataAccessor.ReadVector2(); } jsonAttribute = jsonAttributes["COLOR_0"]; if (jsonAttribute != null) { _dataAccessor.Seek(jsonAttribute.AsInt); results.colors = _dataAccessor.ReadColor(); } jsonAttribute = jsonAttributes["WEIGHTS_0"]; if (jsonAttribute != null) { results.boneWeights = new BoneWeight[totalVertexCount]; _dataAccessor.Seek(jsonAttribute.AsInt); _dataAccessor.ReadWeights(ref results.boneWeights); var jointAttribute = jsonAttributes["JOINTS_0"]; _dataAccessor.Seek(jointAttribute.AsInt); _dataAccessor.ReadJoints(ref results.boneWeights); } return results; } private void ProcessSkin(JSONNode skinNode, SkinnedMeshRenderer renderer) { Matrix4x4[] inverseBindMatrices = null; if (skinNode["inverseBindMatrices"] != null) { var inverseBindMatricesId = skinNode["inverseBindMatrices"].AsInt; _dataAccessor.Seek(inverseBindMatricesId); inverseBindMatrices = _dataAccessor.ReadMatrix4x4(GLTFToUnitySpace); } if (skinNode["skeleton"] != null) { var skeletonRootId = skinNode["skeleton"].AsInt; renderer.rootBone = m_Nodes[skeletonRootId].transform; } Transform[] bones = null; if (skinNode["joints"] != null) { var joints = skinNode["joints"].AsArray; bones = new Transform[joints.Count]; for (int i = 0; i < joints.Count; i++) { bones[i] = m_Nodes[joints[i]].transform; } } renderer.sharedMesh.bindposes = inverseBindMatrices; renderer.bones = bones; } private OVRMaterialData ProcessMaterial(int matId) { OVRMaterialData matData = new OVRMaterialData(); var jsonMaterial = m_jsonData["materials"][matId]; var jsonAlphaMode = jsonMaterial["alphaMode"]; bool alphaBlendMode = jsonAlphaMode != null && jsonAlphaMode.Value == "BLEND"; var jsonPbrDetails = jsonMaterial["pbrMetallicRoughness"]; matData.baseColorFactor = Color.white; // GLTF Default var jsonBaseColorFactor = jsonPbrDetails["baseColorFactor"]; if (jsonBaseColorFactor != null) { matData.baseColorFactor = new Color(jsonBaseColorFactor[0].AsFloat, jsonBaseColorFactor[1].AsFloat, jsonBaseColorFactor[2].AsFloat, jsonBaseColorFactor[3].AsFloat); } var jsonBaseColor = jsonPbrDetails["baseColorTexture"]; if (jsonBaseColor != null) { int textureId = jsonBaseColor["index"].AsInt; matData.textureId = textureId; } else { var jsonTextrure = jsonMaterial["emissiveTexture"]; if (jsonTextrure != null) { int textureId = jsonTextrure["index"].AsInt; matData.textureId = textureId; } } matData.shader = alphaBlendMode ? m_AlphaBlendShader : m_Shader; return matData; } private OVRTextureData ProcessTexture(int textureId) { var jsonTexture = m_jsonData["textures"][textureId]; int imageSource = -1; var jsonExtensions = jsonTexture["extensions"]; if (jsonExtensions != null) { var baisuExtension = jsonExtensions["KHR_texture_basisu"]; if (baisuExtension != null) { imageSource = baisuExtension["source"].AsInt; } } else { imageSource = jsonTexture["source"].AsInt; } var jsonSource = m_jsonData["images"][imageSource]; OVRTextureData textureData = new OVRTextureData(); var jsonSourceUri = jsonSource["uri"].Value; if (!String.IsNullOrEmpty(jsonSourceUri)) { textureData.uri = jsonSourceUri; return textureData; } // skip "sampler". not supported var bufferViewId = jsonSource["bufferView"].AsInt; switch (jsonSource["mimeType"].Value) { case "image/ktx2": textureData.data = _dataAccessor.ReadBuffer(bufferViewId); textureData.format = OVRTextureFormat.KTX2; break; case "image/png": textureData.data = _dataAccessor.ReadBuffer(bufferViewId); textureData.format = OVRTextureFormat.PNG; break; default: Debug.LogWarning($"Unsupported image mimeType '{jsonSource["mimeType"].Value}'"); break; } return textureData; } private void TranscodeTexture(ref OVRTextureData textureData) { if (!String.IsNullOrEmpty(textureData.uri)) { return; } if (textureData.format == OVRTextureFormat.KTX2) { OVRKtxTexture.Load(textureData.data, ref textureData); } else if (textureData.format == OVRTextureFormat.PNG) { // fall back to unity Texture2D.LoadImage, which will override dimensions & format. } else { Debug.LogWarning("Only KTX2 textures can be trascoded."); } } private Material CreateUnityMaterial(OVRMaterialData matData, bool loadMips) { Material mat = new Material(matData.shader); mat.color = matData.baseColorFactor; if (loadMips && mat.HasProperty("_MainTexMMBias")) mat.SetFloat("_MainTexMMBias", m_TextureMipmapBias); Texture2D texture = null; bool configureCreatedTexture = false; if (m_textures.TryGetValue(matData.textureId, out texture)) { mat.mainTexture = texture; return mat; } if (matData.texture.format == OVRTextureFormat.KTX2) { texture = new Texture2D(matData.texture.width, matData.texture.height, matData.texture.transcodedFormat, loadMips); texture.LoadRawTextureData(matData.texture.data); configureCreatedTexture = true; } else if (matData.texture.format == OVRTextureFormat.PNG) { texture = new Texture2D(2, 2, TextureFormat.RGBA32, loadMips); texture.LoadImage(matData.texture.data); configureCreatedTexture = true; } else if (!String.IsNullOrEmpty(matData.texture.uri)) { texture = textureUriHandler?.Invoke(matData.texture.uri, mat); } if (!texture) return mat; if (configureCreatedTexture) { ApplyTextureQuality(m_TextureQuality, ref texture); texture.Apply(updateMipmaps: false, makeNoLongerReadable: true); } m_textures[matData.textureId] = texture; mat.mainTexture = texture; return mat; } private OVRGLTFInputNode GetInputNodeType(string name) { foreach (var item in InputNodeNameMap) { if (name.Contains(item.Key)) { return item.Value; } } return OVRGLTFInputNode.None; } private IEnumerator ProcessAnimations() { var animations = m_jsonData["animations"]; var animationIndex = 0; var processingStart = Time.realtimeSinceStartup; foreach (JSONNode animation in animations.AsArray) { //We don't need animation name at this moment //string name = animation["name"].ToString(); var animationNodeLookup = new Dictionary(); var channels = animation["channels"].AsArray; foreach (JSONNode channel in channels) { int nodeId = channel["target"]["node"].AsInt; OVRGLTFInputNode inputNodeType = GetInputNodeType(m_Nodes[nodeId].name); if (!animationNodeLookup.TryGetValue(nodeId, out var animationNode)) { m_morphTargetHandlers.TryGetValue(nodeId, out var morphTargetHandler); animationNode = animationNodeLookup[nodeId] = new OVRGLTFAnimatinonNode( inputNodeType, m_Nodes[nodeId], morphTargetHandler); } if (inputNodeType != OVRGLTFInputNode.None) { if (!m_InputAnimationNodes.ContainsKey(inputNodeType)) { m_InputAnimationNodes[inputNodeType] = animationNode; } } animationNode.AddChannel(channel, animation["samplers"], _dataAccessor); } m_AnimationLookup[animationIndex] = animationNodeLookup.Values.ToArray(); animationIndex++; if (Time.realtimeSinceStartup - processingStart > LoadingMaxTimePerFrame) { processingStart = Time.realtimeSinceStartup; yield return null; } } } }