From a6dff99ed59112c03a7132e89dc7c65fdeffe0d8 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Mon, 2 Aug 2021 10:26:19 -0700 Subject: [PATCH] refactored gltf loading a bit (#13) # XRTK - Mixed Reality Toolkit Pull Request ## Overview Refactored GLTF loading a bit to handle async loading a tad bit better. Also loads local resources using webrequest to keep from having multiple code paths for different platforms --- Runtime/Serialization/ConstructGltf.cs | 259 +++---------------------- Runtime/Serialization/GltfUtility.cs | 135 +++++++------ csc.rsp | 3 + csc.rsp.meta | 7 + package.json | 11 +- 5 files changed, 116 insertions(+), 299 deletions(-) create mode 100644 csc.rsp create mode 100644 csc.rsp.meta diff --git a/Runtime/Serialization/ConstructGltf.cs b/Runtime/Serialization/ConstructGltf.cs index 0f1927a..9878a20 100644 --- a/Runtime/Serialization/ConstructGltf.cs +++ b/Runtime/Serialization/ConstructGltf.cs @@ -1,9 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) XRTK. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Rendering; @@ -26,29 +25,23 @@ public static class ConstructGltf private static readonly int GlossinessId = Shader.PropertyToID("_Glossiness"); private static readonly int MetallicId = Shader.PropertyToID("_Metallic"); private static readonly int BumpMapId = Shader.PropertyToID("_BumpMap"); - private static readonly int EmissiveColorId = Shader.PropertyToID("_EmissiveColor"); - private static readonly int ChannelMapId = Shader.PropertyToID("_ChannelMap"); - private static readonly int SmoothnessId = Shader.PropertyToID("_Smoothness"); - private static readonly int NormalMapId = Shader.PropertyToID("_NormalMap"); - private static readonly int NormalMapScaleId = Shader.PropertyToID("_NormalMapScale"); - private static readonly int CullModeId = Shader.PropertyToID("_CullMode"); /// /// Constructs the glTF Object. /// /// + /// /// The new of the final constructed - public static async void Construct(this GltfObject gltfObject) - { - await gltfObject.ConstructAsync(); - } + public static async void Construct(this GltfObject gltfObject, bool setActive = true) + => await gltfObject.ConstructAsync(setActive); /// /// Constructs the glTF Object. /// /// + /// /// The new of the final constructed - public static async Task ConstructAsync(this GltfObject gltfObject) + public static async Task ConstructAsync(this GltfObject gltfObject, bool setActive = true) { if (!gltfObject.asset.version.Contains("2.0")) { @@ -61,10 +54,7 @@ public static async Task ConstructAsync(this GltfObject gltfObject) var rootObject = new GameObject($"glTF Scene {gltfObject.Name}"); rootObject.SetActive(false); - if (gltfObject.LoadAsynchronously) - { - await Awaiters.BackgroundThread; - } + if (gltfObject.LoadAsynchronously) { await Awaiters.BackgroundThread; } for (int i = 0; i < gltfObject.bufferViews?.Length; i++) { @@ -93,11 +83,11 @@ public static async Task ConstructAsync(this GltfObject gltfObject) await gltfObject.ConstructSceneAsync(gltfObject.scenes[i], rootObject); } - rootObject.SetActive(true); + rootObject.SetActive(setActive); return gltfObject.GameObjectReference = rootObject; } - private async static Task ConstructBufferView(this GltfObject gltfObject, GltfBufferView bufferView) + private static async Task ConstructBufferView(this GltfObject gltfObject, GltfBufferView bufferView) { bufferView.Buffer = gltfObject.buffers[bufferView.buffer]; @@ -105,111 +95,41 @@ private async static Task ConstructBufferView(this GltfObject gltfObject, GltfBu !string.IsNullOrEmpty(gltfObject.Uri) && !string.IsNullOrEmpty(bufferView.Buffer.uri)) { - if (gltfObject.Uri.ToLower().StartsWith("http")) - { - var path = gltfObject.Uri.PathFromURI(); - var fullpath = path + bufferView.Buffer.uri; - var response = await Rest.GetAsync(fullpath); - if (response.Successful) - { - bufferView.Buffer.BufferData = response.ResponseData; - } - } - else + var path = gltfObject.Uri.PathFromURI(); + var loadTask = Rest.GetAsync($"{path}{bufferView.Buffer.uri}"); + var response = gltfObject.LoadAsynchronously ? await loadTask : loadTask.Result; + + if (response.Successful) { - var parentDirectory = Directory.GetParent(gltfObject.Uri).FullName; - bufferView.Buffer.BufferData = File.ReadAllBytes($"{parentDirectory}\\{bufferView.Buffer.uri}"); + bufferView.Buffer.BufferData = response.ResponseData; } } } private static async Task ConstructTextureAsync(this GltfObject gltfObject, GltfTexture gltfTexture) { - if (gltfObject.LoadAsynchronously) - { - await Awaiters.BackgroundThread; - } + if (gltfObject.LoadAsynchronously) { await Awaiters.BackgroundThread; } if (gltfTexture.source >= 0) { - GltfImage gltfImage = gltfObject.images[gltfTexture.source]; + var gltfImage = gltfObject.images[gltfTexture.source]; byte[] imageData = null; Texture2D texture = null; - if (!string.IsNullOrEmpty(gltfObject.Uri) && !string.IsNullOrEmpty(gltfImage.uri)) + if (!string.IsNullOrEmpty(gltfObject.Uri) && + !string.IsNullOrEmpty(gltfImage.uri)) { - //TODO - Replace with Rest call - if (gltfObject.Uri.ToLower().StartsWith("http")) + var path = gltfObject.Uri.PathFromURI(); + var textureLoadTask = Rest.DownloadTextureAsync($"{path}{gltfImage.uri}"); + + if (gltfObject.LoadAsynchronously) { - var path = gltfObject.Uri.PathFromURI(); - var fullpath = path + gltfImage.uri; - var response = await Rest.GetAsync(fullpath); - if (response.Successful) - { - imageData = response.ResponseData; - } + texture = await textureLoadTask; } else { - var parentDirectory = Directory.GetParent(gltfObject.Uri).FullName; - var path = $"{parentDirectory}\\{gltfImage.uri}"; - -#if UNITY_EDITOR - if (gltfObject.LoadAsynchronously) { await Awaiters.UnityMainThread; } - var projectPath = path.Replace("\\", "/"); - projectPath = projectPath.Replace(Application.dataPath, "Assets"); - texture = UnityEditor.AssetDatabase.LoadAssetAtPath(projectPath); - - if (gltfObject.LoadAsynchronously) { await Awaiters.BackgroundThread; } -#endif - - if (texture == null) - { -#if WINDOWS_UWP - if (gltfObject.LoadAsynchronously) - { - try - { - var storageFile = await Windows.Storage.StorageFile.GetFileFromPathAsync(path); - - if (storageFile != null) - { - - var buffer = await Windows.Storage.FileIO.ReadBufferAsync(storageFile); - - using (Windows.Storage.Streams.DataReader dataReader = Windows.Storage.Streams.DataReader.FromBuffer(buffer)) - { - imageData = new byte[buffer.Length]; - dataReader.ReadBytes(imageData); - } - } - } - catch (Exception e) - { - Debug.LogError(e.Message); - } - } - else - { - imageData = UnityEngine.Windows.File.ReadAllBytes(path); - } -#else - using (FileStream stream = File.Open(path, FileMode.Open)) - { - imageData = new byte[stream.Length]; - - if (gltfObject.LoadAsynchronously) - { - await stream.ReadAsync(imageData, 0, (int)stream.Length); - } - else - { - stream.Read(imageData, 0, (int)stream.Length); - } - } -#endif - } + texture = textureLoadTask.GetAwaiter().GetResult(); } } else @@ -222,7 +142,7 @@ private static async Task ConstructTextureAsync(this GltfObject gltfObject, Gltf if (texture == null) { if (gltfObject.LoadAsynchronously) { await Awaiters.UnityMainThread; } - // TODO Load texture async + // TODO Load texture async from native plugin? texture = new Texture2D(2, 2); gltfImage.Texture = texture; gltfImage.Texture.LoadImage(imageData); @@ -232,7 +152,7 @@ private static async Task ConstructTextureAsync(this GltfObject gltfObject, Gltf gltfImage.Texture = texture; } - gltfTexture.Texture = texture; + gltfTexture.Texture = gltfImage.Texture; if (gltfObject.LoadAsynchronously) { await Awaiters.BackgroundThread; } } @@ -242,7 +162,7 @@ private static async Task ConstructMaterialAsync(this GltfObject gltfObject, Glt { if (gltfObject.LoadAsynchronously) { await Awaiters.UnityMainThread; } - Material material = await CreateStandardShaderMaterial(gltfObject, gltfMaterial, materialId); + var material = await CreateStandardShaderMaterial(gltfObject, gltfMaterial, materialId); if (material == null) { @@ -256,120 +176,6 @@ private static async Task ConstructMaterialAsync(this GltfObject gltfObject, Glt if (gltfObject.LoadAsynchronously) { await Awaiters.BackgroundThread; } } - private static async Task CreateMRTKShaderMaterial(GltfObject gltfObject, GltfMaterial gltfMaterial, int materialId) - { - var shader = Shader.Find("Mixed Reality Toolkit/Standard"); - - if (shader == null) { return null; } - - var material = new Material(shader) - { - name = string.IsNullOrEmpty(gltfMaterial.name) ? $"glTF Material {materialId}" : gltfMaterial.name - }; - - if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) - { - material.mainTexture = gltfObject.images[gltfMaterial.pbrMetallicRoughness.baseColorTexture.index].Texture; - } - - material.color = gltfMaterial.pbrMetallicRoughness.baseColorFactor.GetColorValue(); - - if (gltfMaterial.alphaMode == "MASK") - { - material.SetInt(SrcBlendId, (int)BlendMode.One); - material.SetInt(DstBlendId, (int)BlendMode.Zero); - material.SetInt(ZWriteId, 1); - material.SetInt(ModeId, 3); - material.SetOverrideTag("RenderType", "Cutout"); - material.EnableKeyword("_ALPHATEST_ON"); - material.DisableKeyword("_ALPHABLEND_ON"); - material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); - material.renderQueue = 2450; - } - else if (gltfMaterial.alphaMode == "BLEND") - { - material.SetInt(SrcBlendId, (int)BlendMode.One); - material.SetInt(DstBlendId, (int)BlendMode.OneMinusSrcAlpha); - material.SetInt(ZWriteId, 0); - material.SetInt(ModeId, 3); - material.SetOverrideTag("RenderType", "Transparency"); - material.DisableKeyword("_ALPHATEST_ON"); - material.DisableKeyword("_ALPHABLEND_ON"); - material.EnableKeyword("_ALPHAPREMULTIPLY_ON"); - material.renderQueue = 3000; - } - - if (gltfMaterial.emissiveTexture.index >= 0 && material.HasProperty("_EmissionMap")) - { - material.EnableKeyword("_EMISSION"); - material.SetColor(EmissiveColorId, gltfMaterial.emissiveFactor.GetColorValue()); - } - - if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) - { - var texture = gltfObject.images[gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index].Texture; - - Texture2D occlusionTexture = null; - if (gltfMaterial.occlusionTexture.index >= 0) - { - occlusionTexture = gltfObject.images[gltfMaterial.occlusionTexture.index].Texture; - } - - if (texture.isReadable) - { - var pixels = texture.GetPixels(); - Color[] occlusionPixels = null; - if (occlusionTexture != null && - occlusionTexture.isReadable) - { - occlusionPixels = occlusionTexture.GetPixels(); - } - - if (gltfObject.LoadAsynchronously) { await Awaiters.BackgroundThread; } - - var pixelCache = new Color[pixels.Length]; - - for (int c = 0; c < pixels.Length; c++) - { - pixelCache[c].r = pixels[c].b; // MRTK standard shader metallic value, glTF metallic value - pixelCache[c].g = occlusionPixels?[c].r ?? 1.0f; // MRTK standard shader occlusion value, glTF occlusion value if available - pixelCache[c].b = 0f; // MRTK standard shader emission value - pixelCache[c].a = (1.0f - pixels[c].g); // MRTK standard shader smoothness value, invert of glTF roughness value - } - - if (gltfObject.LoadAsynchronously) { await Awaiters.UnityMainThread; } - texture.SetPixels(pixelCache); - texture.Apply(); - - material.SetTexture(ChannelMapId, texture); - material.EnableKeyword("_CHANNEL_MAP"); - } - else - { - material.DisableKeyword("_CHANNEL_MAP"); - } - - material.SetFloat(SmoothnessId, Mathf.Abs((float)gltfMaterial.pbrMetallicRoughness.roughnessFactor - 1f)); - material.SetFloat(MetallicId, (float)gltfMaterial.pbrMetallicRoughness.metallicFactor); - } - - - if (gltfMaterial.normalTexture.index >= 0) - { - material.SetTexture(NormalMapId, gltfObject.images[gltfMaterial.normalTexture.index].Texture); - material.SetFloat(NormalMapScaleId, (float)gltfMaterial.normalTexture.scale); - material.EnableKeyword("_NORMAL_MAP"); - } - - if (gltfMaterial.doubleSided) - { - material.SetFloat(CullModeId, (float)UnityEngine.Rendering.CullMode.Off); - } - - material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; - return material; - } - private static async Task CreateStandardShaderMaterial(GltfObject gltfObject, GltfMaterial gltfMaterial, int materialId) { var shader = Shader.Find("Standard"); @@ -486,7 +292,7 @@ private static async Task ConstructNodeAsync(GltfObject gltfObject, GltfNode nod if (gltfObject.LoadAsynchronously) { await Awaiters.BackgroundThread; } - node.Matrix = node.GetTrsProperties(out Vector3 position, out Quaternion rotation, out Vector3 scale); + node.Matrix = node.GetTrsProperties(out var position, out var rotation, out var scale); if (node.Matrix == Matrix4x4.identity) { @@ -531,10 +337,9 @@ private static async Task ConstructNodeAsync(GltfObject gltfObject, GltfNode nod private static async Task ConstructMeshAsync(GltfObject gltfObject, GameObject parent, int meshId) { - GltfMesh gltfMesh = gltfObject.meshes[meshId]; - - var renderer = parent.gameObject.AddComponent(); + var gltfMesh = gltfObject.meshes[meshId]; var filter = parent.gameObject.AddComponent(); + var renderer = parent.gameObject.AddComponent(); if (gltfMesh.primitives.Length == 1) { @@ -666,7 +471,7 @@ private static async Task ConstructMeshPrimitiveAsync(GltfObject gltfObjec var mesh = new Mesh { - indexFormat = vertexCount > UInt16.MaxValue ? IndexFormat.UInt32 : IndexFormat.UInt16, + indexFormat = vertexCount > ushort.MaxValue ? IndexFormat.UInt32 : IndexFormat.UInt16, }; if (positionAccessor != null) diff --git a/Runtime/Serialization/GltfUtility.cs b/Runtime/Serialization/GltfUtility.cs index 664bd40..02ba45a 100644 --- a/Runtime/Serialization/GltfUtility.cs +++ b/Runtime/Serialization/GltfUtility.cs @@ -1,16 +1,20 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) XRTK. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using UnityEngine; +using XRTK.Extensions; using XRTK.Utilities.Async; using XRTK.Utilities.Async.Internal; using XRTK.Utilities.Gltf.Schema; +using XRTK.Utilities.WebRequestRest; namespace XRTK.Utilities.Gltf.Serialization { @@ -21,13 +25,14 @@ public static class GltfUtility /// /// Imports a glTF object from the provided uri. /// - /// the path to the file to load + /// The path to the file to load. + /// Set the gltf active as soon as it's been loaded. /// New imported from uri. /// /// Must be called from the main thread. /// If the is false, then this method will run synchronously. /// - public static async Task ImportGltfObjectFromPathAsync(string uri) + public static async Task ImportGltfObjectFromPathAsync(string uri, bool setActive = true) { if (!SyncContextUtility.IsMainThread) { @@ -41,78 +46,82 @@ public static async Task ImportGltfObjectFromPathAsync(string uri) return null; } + string gltfPath; GltfObject gltfObject; - bool isGlb = false; - bool loadAsynchronously = Application.isPlaying; + + var isGlb = false; + var loadAsynchronously = Application.isPlaying; if (loadAsynchronously) { await Awaiters.BackgroundThread; } - if (uri.EndsWith(".gltf")) + if (uri.Contains(".zip")) { - string gltfJson = File.ReadAllText(uri); - - gltfObject = GetGltfObjectFromJson(gltfJson); + var directory = Path.GetFullPath(uri.Replace(".zip", string.Empty)).ForwardSlashes(); - if (gltfObject == null) - { - Debug.LogError("Failed load Gltf Object from json schema."); - return null; - } - } - else if (uri.EndsWith(".glb")) - { - isGlb = true; - byte[] glbData; - -#if WINDOWS_UWP - - if (loadAsynchronously) + if (!Directory.Exists(directory)) { try { - var storageFile = await Windows.Storage.StorageFile.GetFileFromPathAsync(uri); - - if (storageFile == null) - { - Debug.LogError($"Failed to locate .glb file at {uri}"); - return null; - } - - var buffer = await Windows.Storage.FileIO.ReadBufferAsync(storageFile); - - using (Windows.Storage.Streams.DataReader dataReader = Windows.Storage.Streams.DataReader.FromBuffer(buffer)) - { - glbData = new byte[buffer.Length]; - dataReader.ReadBytes(glbData); - } + ZipFile.ExtractToDirectory(uri, directory); } catch (Exception e) { - Debug.LogError(e.Message); + Debug.LogError($"Failed to unzip {uri}\n{e}"); return null; } } - else + + gltfPath = Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly) + .FirstOrDefault(extension => + extension.Contains(".gltf") || + extension.Contains(".glb")); + } + else + { + gltfPath = uri; + } + + if (string.IsNullOrWhiteSpace(gltfPath)) + { + Debug.LogError($"No gltf file found @\"{uri}\"!"); + return null; + } + + var loadResourceTask = Rest.GetAsync(gltfPath); + var response = loadAsynchronously + ? await loadResourceTask + : loadResourceTask.Result; + + if (!response.Successful) + { + Debug.LogError($"Error loading gltf object!\n{response.ResponseBody}"); + return null; + } + + if (gltfPath.EndsWith(".gltf")) + { + var json = string.IsNullOrWhiteSpace(response.ResponseBody) + ? Encoding.UTF8.GetString(response.ResponseData) + : response.ResponseBody; + + if (string.IsNullOrWhiteSpace(json)) { - glbData = UnityEngine.Windows.File.ReadAllBytes(uri); + Debug.Log($"Failed to decode gltf json form {uri}!"); + return null; } -#else - using (var stream = File.Open(uri, FileMode.Open)) - { - glbData = new byte[stream.Length]; - if (loadAsynchronously) - { - await stream.ReadAsync(glbData, 0, (int)stream.Length); - } - else - { - stream.Read(glbData, 0, (int)stream.Length); - } - } -#endif + gltfObject = GetGltfObjectFromJson(json); - gltfObject = GetGltfObjectFromGlb(glbData); + if (gltfObject == null) + { + Debug.LogError("Failed load Gltf Object from json schema."); + return null; + } + } + else if (gltfPath.EndsWith(".glb")) + { + isGlb = true; + gltfObject = GetGltfObjectFromGlb(response.ResponseData); if (gltfObject == null) { @@ -126,15 +135,13 @@ public static async Task ImportGltfObjectFromPathAsync(string uri) return null; } - gltfObject.Uri = uri; - int nameStart = uri.Replace("\\", "/").LastIndexOf("/", StringComparison.Ordinal) + 1; - int nameLength = uri.Length - nameStart; - gltfObject.Name = uri.Substring(nameStart, nameLength).Replace(isGlb ? ".glb" : ".gltf", string.Empty); - + gltfObject.Uri = gltfPath; + gltfObject.Name = gltfObject.Uri.FilenameFromURI().Replace(isGlb ? ".glb" : ".gltf", string.Empty); gltfObject.LoadAsynchronously = loadAsynchronously; - await gltfObject.ConstructAsync(); - if (gltfObject.GameObjectReference == null) + await gltfObject.ConstructAsync(setActive); + + if (gltfObject.GameObjectReference.IsNull()) { Debug.LogError("Failed to construct glTF Object."); } @@ -281,7 +288,7 @@ private static List GetGltfMeshPrimitiveAttributes(string jsonString, Re return jsonObjects; } - MatchCollection matches = regex.Matches(jsonString); + var matches = regex.Matches(jsonString); for (var i = 0; i < matches.Count; i++) { diff --git a/csc.rsp b/csc.rsp new file mode 100644 index 0000000..d289263 --- /dev/null +++ b/csc.rsp @@ -0,0 +1,3 @@ +-warn:4 +-warnaserror+ +-r:System.IO.Compression.FileSystem.dll diff --git a/csc.rsp.meta b/csc.rsp.meta new file mode 100644 index 0000000..3bc60cf --- /dev/null +++ b/csc.rsp.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: acc9f171e2fbe9441b72da71ee4b5af7 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 9ee5d6b..bac5791 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,7 @@ }, "author": "XRTK Team (https://github.com/XRTK)", "dependencies": { - "com.xrtk.core": "0.3.0-preview.1" - }, - "profiles": [ - { - "displayName": "Default Profiles", - "path": "Profiles~/" - } - ] + "com.xrtk.core": "0.3.0-preview.10", + "com.unity.nuget.newtonsoft-json": "2.0.1-preview.1" + } } \ No newline at end of file