using GLTF; using GLTF.Schema; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Unity.Collections; using UnityEngine; using UnityGLTF.Cache; using UnityGLTF.Extensions; using UnityGLTF.Loader; using UnityGLTF.Plugins; using Quaternion = UnityEngine.Quaternion; using Vector3 = UnityEngine.Vector3; #if !WINDOWS_UWP && !UNITY_WEBGL using ThreadPriority = System.Threading.ThreadPriority; #endif using WrapMode = UnityEngine.WrapMode; namespace UnityGLTF { [Flags] public enum DeduplicateOptions { None = 0, Meshes = 1, Textures = 2, } public enum RuntimeTextureCompression { None, LowQuality , HighQuality, } public class ImportOptions { #pragma warning disable CS0618 // Type or member is obsolete public ILoader ExternalDataLoader = null; #pragma warning restore CS0618 // Type or member is obsolete /// /// Optional for loading references from the GLTF to external streams. May also optionally implement . /// public IDataLoader DataLoader = null; public AsyncCoroutineHelper AsyncCoroutineHelper = null; public bool ThrowOnLowMemory = true; public AnimationMethod AnimationMethod = AnimationMethod.Legacy; public bool AnimationLoopTime = true; public bool AnimationLoopPose = false; public DeduplicateOptions DeduplicateResources = DeduplicateOptions.None; public bool SwapUVs = false; public GLTFImporterNormals ImportNormals = GLTFImporterNormals.Import; public GLTFImporterNormals ImportTangents = GLTFImporterNormals.Import; public bool ImportBlendShapeNames = true; public CameraImportOption CameraImport = CameraImportOption.ImportAndCameraDisabled; public RuntimeTextureCompression RuntimeTextureCompression = RuntimeTextureCompression.None; public BlendShapeFrameWeightSetting BlendShapeFrameWeight = new BlendShapeFrameWeightSetting(BlendShapeFrameWeightSetting.MultiplierOption.Multiplier1); #if UNITY_EDITOR public GLTFImportContext ImportContext = new GLTFImportContext(null, GLTFSettings.GetOrCreateSettings()); #else public GLTFImportContext ImportContext = new GLTFImportContext(GLTFSettings.GetOrCreateSettings()); #endif [NonSerialized] public ILogger logger; } public enum CameraImportOption { None, ImportAndActive, ImportAndCameraDisabled } public enum AnimationMethod { None, Legacy, Mecanim, MecanimHumanoid, } public struct ImportProgress { public bool IsDownloaded; public int NodeTotal; public int NodeLoaded; public int TextureTotal; public int TextureLoaded; public int BuffersTotal; public int BuffersLoaded; public float Progress { get { int total = NodeTotal + TextureTotal + BuffersTotal; int loaded = NodeLoaded + TextureLoaded + BuffersLoaded; if (total > 0) { return (float)loaded / total; } else { return 0.0f; } } } public override string ToString() { return $"{(Progress * 100.0):F2}% (Buffers: {BuffersLoaded}/{BuffersTotal}, Nodes: {NodeLoaded}/{NodeTotal}, Texs: {TextureLoaded}/{TextureTotal})"; } } public struct ImportStatistics { public long TriangleCount; public long VertexCount; } /// /// Converts gltf animation data to unity /// public delegate float[] ValuesConvertion(NumericArray data, int frame); public partial class GLTFSceneImporter : IDisposable { public enum ColliderType { None, Box, Mesh, MeshConvex } protected struct GLBStream { public Stream Stream; public long StartPosition; } /// /// Maximum LOD /// public int MaximumLod = 300; /// /// Timeout for certain threading operations /// public int Timeout = 8; private bool _isMultithreaded; /// /// Use Multithreading or not. /// In editor, this is always false. This is to prevent a freeze in editor (noticed in Unity versions 2017.x and 2018.x) /// public bool IsMultithreaded { get { return (Application.isEditor || Application.platform == RuntimePlatform.WebGLPlayer) ? false : _isMultithreaded; } set { _isMultithreaded = value; } } public GLTFRoot Root => _gltfRoot; /// /// The parent transform for the created GameObject /// public Transform SceneParent { get; set; } /// /// The last created object /// public GameObject CreatedObject { get; private set; } /// /// All created animation clips /// public AnimationClip[] CreatedAnimationClips { get; private set; } /// /// Adds colliders to primitive objects when created /// public ColliderType Collider { get; set; } /// /// Override for the shader to use on created materials /// public string CustomShaderName { get; set; } public GameObject LastLoadedScene { get { return _lastLoadedScene; } } private bool AnyAnimationTimeNotIncreasing; public TextureCacheData[] TextureCache => _assetCache.TextureCache; public Texture2D[] InvalidImageCache => _assetCache.InvalidImageCache; public MaterialCacheData[] MaterialCache => _assetCache.MaterialCache; public AnimationCacheData[] AnimationCache => _assetCache.AnimationCache; public GameObject[] NodeCache => _assetCache.NodeCache; public MeshCacheData[] MeshCache => _assetCache.MeshCache; private Dictionary> _nativeBuffers = new Dictionary>(); #if HAVE_MESHOPT_DECOMPRESS private List> meshOptNativeBuffers = new List>(); #endif /// /// Whether to keep a CPU-side copy of the mesh after upload to GPU (for example, in case normals/tangents need recalculation) /// public bool KeepCPUCopyOfMesh = true; /// /// Whether to keep a CPU-side copy of the texture after upload to GPU /// /// /// This is necessary when a texture is used with different sampler states, as Unity doesn't allow setting /// of filter and wrap modes separately form the texture object. Setting this to false will omit making a copy /// of a texture in that case and use the original texture's sampler state wherever it's referenced; this is /// appropriate in cases such as the filter and wrap modes being specified in the shader instead /// public bool KeepCPUCopyOfTexture = true; /// /// Specifies whether the MipMap chain should be generated for model textures /// public bool GenerateMipMapsForTextures = true; /// /// When screen coverage is above threshold and no LOD mesh, cull the object /// public bool CullFarLOD = false; public bool IsRunning => _isRunning; public bool LoadUnreferencedImagesAndMaterials = false; /// /// Statistics from the scene /// public ImportStatistics Statistics; protected GLTFImportContext Context => _options.ImportContext; protected ImportOptions _options; protected MemoryChecker _memoryChecker; protected GameObject _lastLoadedScene; protected readonly GLTFMaterial DefaultMaterial; internal MaterialCacheData _defaultLoadedMaterial = null; protected string _gltfFileName; protected GLBStream _gltfStream; protected GLTFRoot _gltfRoot; protected AssetCache _assetCache; protected bool _isRunning = false; protected ImportProgress progressStatus = default(ImportProgress); protected IProgress progress = null; private static ILogger Debug = UnityEngine.Debug.unityLogger; protected ColorSpace _activeColorSpace; public GLTFSceneImporter(string gltfFileName, ImportOptions options) : this(options) { _gltfFileName = gltfFileName; VerifyDataLoader(); } public GLTFSceneImporter(GLTFRoot rootNode, Stream gltfStream, ImportOptions options) : this(options) { _gltfRoot = rootNode; if (gltfStream != null) { _gltfStream = new GLBStream { Stream = gltfStream, StartPosition = gltfStream.Position }; } VerifyDataLoader(); } /// /// Loads a glTF file from a stream. It's recommended to load only gltf data without any external references. /// /// /// /// var stream = new FileStream(filePath, FileMode.Open); /// var importOptions = new ImportOptions(); /// var importer = new GLTFSceneImporter(stream, importOptions); /// await importer.LoadSceneAsync(); /// stream.Dispose(); /// /// public GLTFSceneImporter(Stream gltfStream, ImportOptions options) : this(options) { if (gltfStream != null) { _gltfStream = new GLBStream { Stream = gltfStream, StartPosition = gltfStream.Position }; } GLTFParser.ParseJson(_gltfStream.Stream, out _gltfRoot, _gltfStream.StartPosition); VerifyDataLoader(); } private GLTFSceneImporter(ImportOptions options) { if (options.ImportContext != null) { options.ImportContext.SceneImporter = this; } if (options.logger != null) Debug = options.logger; else Debug = UnityEngine.Debug.unityLogger; DefaultMaterial = new GLTFMaterial { Name = "Default", AlphaMode = AlphaMode.OPAQUE, DoubleSided = false, PbrMetallicRoughness = new PbrMetallicRoughness { MetallicFactor = 1, RoughnessFactor = 1, } }; _activeColorSpace = QualitySettings.activeColorSpace; _options = options; } private NativeArray GetOrCreateNativeBuffer(Stream stream) { if (_nativeBuffers.TryGetValue(stream, out var buffer)) { return buffer; } var buf = new byte[stream.Length]; stream.Position = 0; long remainingSize = stream.Length; while (remainingSize != 0) { int sizeToLoad = (int)System.Math.Min(remainingSize, int.MaxValue); sizeToLoad = stream.Read(buf, (int)(stream.Length - remainingSize), sizeToLoad); remainingSize -= (uint)sizeToLoad; if (sizeToLoad == 0 && remainingSize > 0) { throw new Exception($"Unexpected end of stream while loading buffer view (File: {_gltfFileName})"); } } var newNativeBuffer = new NativeArray(buf, Allocator.Persistent); _nativeBuffers.Add(stream,newNativeBuffer); return newNativeBuffer; } private void VerifyDataLoader() { if (_options.DataLoader == null) { if (_options.ExternalDataLoader == null) { if (string.IsNullOrEmpty(_gltfFileName)) { Debug.Log(LogType.Warning, "No filename specified for GLTFSceneImporter, external references will not be loaded"); return; } _options.DataLoader = new UnityWebRequestLoader(URIHelper.GetDirectoryName(_gltfFileName)); _gltfFileName = URIHelper.GetFileFromUri(new Uri(_gltfFileName)); } else _options.DataLoader = LegacyLoaderWrapper.Wrap(_options.ExternalDataLoader); } } public void Dispose() { Cleanup(); DisposeNativeBuffers(); } /// /// Loads a glTF Scene into the LastLoadedScene field /// /// The scene to load, If the index isn't specified, we use the default index in the file. Failing that we load index 0. /// /// Callback function for when load is completed /// Cancellation token for loading /// public async Task LoadSceneAsync(int sceneIndex = -1, bool showSceneObj = true, Action onLoadComplete = null, CancellationToken cancellationToken = default(CancellationToken), IProgress progress = null) { try { lock (this) { if (_isRunning) { throw new GLTFLoadException($"Cannot call LoadScene while GLTFSceneImporter is already running (File: {_gltfFileName})"); } _isRunning = true; } // TODO check where the right place is to call OnBeforeImport as early as possible foreach (var plugin in Context.Plugins) { plugin.OnBeforeImport(); } if (_options.ThrowOnLowMemory) { _memoryChecker = new MemoryChecker(); } this.progressStatus = new ImportProgress(); this.progress = progress; Statistics = new ImportStatistics(); progress?.Report(progressStatus); #if UNITY_EDITOR // When loading from a buffer, this is not set; sanitizing that here // so we can log proper file names later on if (_gltfFileName == null) { var importSource = _options?.ImportContext?.AssetContext?.assetPath; if (importSource != null) _gltfFileName = importSource; } #endif if (_gltfRoot == null) { foreach (var plugin in Context.Plugins) plugin.OnBeforeImportRoot(); await LoadJson(_gltfFileName); progressStatus.IsDownloaded = true; } foreach (var plugin in Context.Plugins) { plugin.OnAfterImportRoot(_gltfRoot); } cancellationToken.ThrowIfCancellationRequested(); if (_assetCache == null) { _assetCache = new AssetCache(_gltfRoot); } #if HAVE_MESHOPT_DECOMPRESS if (Context.TryGetPlugin(out _)) { await MeshOptDecodeBuffer(_gltfRoot); } #endif await _LoadScene(sceneIndex, showSceneObj, cancellationToken); // for Editor import, we also want to load unreferenced assets that wouldn't be loaded at runtime if (LoadUnreferencedImagesAndMaterials) await LoadUnreferencedAssetsAsync(); } catch (Exception ex) { Cleanup(); DisposeNativeBuffers(); onLoadComplete?.Invoke(null, ExceptionDispatchInfo.Capture(ex)); Debug.Log(LogType.Error, $"Error loading file: {_gltfFileName}" + System.Environment.NewLine + "Message: " + ex.Message + System.Environment.NewLine + "StackTrace: " + ex.StackTrace); throw; } finally { lock (this) { _isRunning = false; } } _gltfStream.Stream.Close(); DisposeNativeBuffers(); if (this.progress != null) await Task.Yield(); onLoadComplete?.Invoke(LastLoadedScene, null); } private async Task LoadUnreferencedAssetsAsync() { for (int i = 0; i < TextureCache.Length; i++) { if (TextureCache[i] == null) { await CreateNotReferencedTexture(i); } } // check which additional materials are in the root, but not yet in the MaterialCache for (var index = 0; index < MaterialCache.Length; index++) { if (_assetCache.MaterialCache[index] == null) { var def = _gltfRoot.Materials[index]; await ConstructMaterialImageBuffers(def); await ConstructMaterial(def, index); } } } public IEnumerator LoadScene(int sceneIndex = -1, bool showSceneObj = true, Action onLoadComplete = null) { return LoadSceneAsync(sceneIndex, showSceneObj, onLoadComplete).AsCoroutine(); } /// /// Loads a node tree from a glTF file into the LastLoadedScene field /// /// The node index to load from the glTF /// public async Task LoadNodeAsync(int nodeIndex, CancellationToken cancellationToken) { await SetupLoad(async () => { CreatedObject = await GetNode(nodeIndex, cancellationToken); InitializeGltfTopLevelObject(); }); } /// /// Load a Material from the glTF by index /// /// /// public virtual async Task LoadMaterialAsync(int materialIndex) { await SetupLoad(async () => { if (materialIndex < 0 || materialIndex >= _gltfRoot.Materials.Count) { throw new ArgumentException($"There is no material for index {materialIndex} (File: {_gltfFileName})"); } if (_assetCache.MaterialCache[materialIndex] == null) { var def = _gltfRoot.Materials[materialIndex]; await ConstructMaterialImageBuffers(def); await ConstructMaterial(def, materialIndex); } }); return _assetCache.MaterialCache[materialIndex].UnityMaterialWithVertexColor; } /// /// Load a Mesh from the glTF by index /// /// /// public virtual async Task LoadMeshAsync(int meshIndex, CancellationToken cancellationToken) { await SetupLoad(async () => { if (meshIndex < 0 || meshIndex >= _gltfRoot.Meshes.Count) { throw new ArgumentException($"There is no mesh for index {meshIndex} (File: {_gltfFileName})"); } if (_assetCache.MeshCache[meshIndex] == null) { var def = _gltfRoot.Meshes[meshIndex]; await ConstructMeshAttributes(def, new MeshId() { Id = meshIndex, Root = _gltfRoot }); await ConstructMesh(def, meshIndex, cancellationToken); } }); return _assetCache.MeshCache[meshIndex].LoadedMesh; } private async Task LoadJson(string jsonFilePath) { #if !WINDOWS_UWP && !UNITY_WEBGL var dataLoader2 = _options.DataLoader as IDataLoader2; if (IsMultithreaded && dataLoader2 != null) { await Task.Run(() => _gltfStream.Stream = dataLoader2.LoadStream(jsonFilePath)); } else #endif { _gltfStream.Stream = await _options.DataLoader.LoadStreamAsync(jsonFilePath); } _gltfStream.StartPosition = 0; #if !WINDOWS_UWP && !UNITY_WEBGL if (IsMultithreaded) { await Task.Run(() => GLTFParser.ParseJson(_gltfStream.Stream, out _gltfRoot, _gltfStream.StartPosition)); if (_gltfRoot == null) { throw new GLTFLoadException($"Failed to parse glTF (File: {_gltfFileName})"); } } else #endif { GLTFParser.ParseJson(_gltfStream.Stream, out _gltfRoot, _gltfStream.StartPosition); } } /// /// Initializes the top-level created node by adding an instantiated GLTF object component to it, /// so that it can cleanup after itself properly when destroyed /// private void InitializeGltfTopLevelObject() { InstantiatedGLTFObject instantiatedGltfObject = CreatedObject.AddComponent(); instantiatedGltfObject.CachedData = new RefCountedCacheData ( _assetCache.MaterialCache, _assetCache.MeshCache, _assetCache.TextureCache, _assetCache.ImageCache, _assetCache.AnimationCache ); } private GameObject NoSceneRoot; /// /// Creates a scene based off loaded JSON. Includes loading in binary and image data to construct the meshes required. /// /// The bufferIndex of scene in gltf file to load /// protected async Task _LoadScene(int sceneIndex = -1, bool showSceneObj = true, CancellationToken cancellationToken = default(CancellationToken)) { GLTFScene scene; if (sceneIndex >= 0 && sceneIndex < _gltfRoot.Scenes.Count) { scene = _gltfRoot.Scenes[sceneIndex]; } else { scene = _gltfRoot.GetDefaultScene(); } if (scene == null) { // throw new GLTFLoadException("No default scene in gltf file."); } try { foreach (var plugin in Context.Plugins) plugin.OnBeforeImportScene(scene); } catch (Exception e) { Debug.LogException(e); } GetGltfContentTotals(scene); await PreparePrimitiveAttributes(); if (IsMultithreaded) await Task.Run(PrepareUnityMeshData, cancellationToken); else PrepareUnityMeshData(); // Free up some Memory, Accessor contents are no longer needed FreeUpAccessorContents(); if (_options.DeduplicateResources != DeduplicateOptions.None) { if (IsMultithreaded) { if (_options.DeduplicateResources.HasFlag(DeduplicateOptions.Meshes)) await Task.Run(CheckForMeshDuplicates, cancellationToken); if (_options.DeduplicateResources.HasFlag(DeduplicateOptions.Textures)) await Task.Run(CheckForDuplicateImages, cancellationToken); } else { if (_options.DeduplicateResources.HasFlag(DeduplicateOptions.Meshes)) CheckForMeshDuplicates(); if (_options.DeduplicateResources.HasFlag(DeduplicateOptions.Textures)) CheckForDuplicateImages(); } } await ConstructScene(scene, showSceneObj, cancellationToken); if (SceneParent != null && CreatedObject) { CreatedObject.transform.SetParent(SceneParent, false); } _lastLoadedScene = CreatedObject; try { foreach (var plugin in Context.Plugins) plugin.OnAfterImportScene(scene, sceneIndex, CreatedObject); } catch (Exception e) { Debug.LogException(e); } } private void GetGltfContentTotals(GLTFScene scene) { // Count Nodes Queue nodeQueue = new Queue(); // Add scene nodes if (scene != null && scene.Nodes != null) { for (int i = 0; i < scene.Nodes.Count; ++i) { nodeQueue.Enqueue(scene.Nodes[i]); } } // BFS of nodes while (nodeQueue.Count > 0) { var cur = nodeQueue.Dequeue(); progressStatus.NodeTotal++; if (cur.Value.Children != null) { for (int i = 0; i < cur.Value.Children.Count; ++i) { nodeQueue.Enqueue(cur.Value.Children[i]); } } } // Total textures progressStatus.TextureTotal += _gltfRoot.Textures?.Count ?? 0; // Total buffers progressStatus.BuffersTotal += _gltfRoot.Buffers?.Count ?? 0; // Send report progress?.Report(progressStatus); } private async Task GetBufferData(BufferId bufferId) { if (_assetCache.BufferCache[bufferId.Id] == null) { await ConstructBuffer(bufferId.Value, bufferId.Id); } return _assetCache.BufferCache[bufferId.Id]; } private float GetLodCoverage(List lodCoverageExtras, int lodIndex) { if (lodCoverageExtras != null && lodIndex < lodCoverageExtras.Count) { return (float)lodCoverageExtras[lodIndex]; } else { return 1.0f / (lodIndex + 2); } } private async Task GetNode(int nodeId, CancellationToken cancellationToken) { try { if (_assetCache.NodeCache[nodeId] == null) { if (nodeId >= _gltfRoot.Nodes.Count) { throw new ArgumentException($"nodeIndex is out of range (File: {_gltfFileName})"); } var node = _gltfRoot.Nodes[nodeId]; cancellationToken.ThrowIfCancellationRequested(); await ConstructBufferData(node, cancellationToken); await ConstructNode(node, nodeId, cancellationToken); try { foreach (var plugin in Context.Plugins) plugin.OnAfterImportNode(node, nodeId, _assetCache.NodeCache[nodeId]); } catch (Exception ex) { Debug.LogException(ex); } // HACK belongs in an extension, but we don't have Importer callbacks yet const string msft_LODExtName = MSFT_LODExtensionFactory.EXTENSION_NAME; if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(msft_LODExtName) && node.Extensions != null && node.Extensions.ContainsKey(msft_LODExtName)) { var lodsExtension = node.Extensions[msft_LODExtName] as MSFT_LODExtension; if (lodsExtension != null && lodsExtension.NodeIds.Count > 0) { for (int i = 0; i < lodsExtension.NodeIds.Count; i++) { int lodNodeId = lodsExtension.NodeIds[i]; await GetNode(lodNodeId, cancellationToken); } } } } return _assetCache.NodeCache[nodeId]; } catch (Exception ex) { // If some failure occured during loading, remove the node if (_assetCache.NodeCache[nodeId] != null) { GameObject.DestroyImmediate(_assetCache.NodeCache[nodeId]); _assetCache.NodeCache[nodeId] = null; } if (ex is OutOfMemoryException) { #if UNITY_2023_1_OR_NEWER await #endif Resources.UnloadUnusedAssets(); } throw; } } private async Task<(Vector3, Quaternion, Vector3)[]> GetInstancesTRS(Node node) { if (Context.TryGetPlugin(out _) && node.Extensions != null && node.Extensions.TryGetValue(EXT_mesh_gpu_instancing_Factory.EXTENSION_NAME, out var ext)) { AttributeAccessor positionsAttr = null; AttributeAccessor rotationAttr = null; AttributeAccessor scaleAttr = null; var extMeshGPUInstancing = ext as EXT_mesh_gpu_instancing; async Task GetAttrAccessorAndAccessorContent(AccessorId accessorId, bool isPosition = false) { var accessor = _gltfRoot.Accessors[accessorId.Id]; var bufferId = accessor.BufferView.Value.Buffer; var bufferData = await GetBufferData(bufferId); var attrAccessor = new AttributeAccessor { AccessorId = accessorId, bufferData = bufferData.bufferData, Offset = (uint)bufferData.ChunkOffset }; GLTFHelpers.LoadBufferView(accessor.BufferView.Value, attrAccessor.Offset, attrAccessor.bufferData, out var bufferViewCache); NumericArray resultArray = attrAccessor.AccessorContent; switch (accessor.Type) { case GLTFAccessorAttributeType.VEC3: if (isPosition) attrAccessor.AccessorId.Value.AsVertexArray(ref resultArray, bufferViewCache); else attrAccessor.AccessorId.Value.AsFloat3Array(ref resultArray, bufferViewCache); break; case GLTFAccessorAttributeType.VEC4: attrAccessor.AccessorId.Value.AsFloat4Array(ref resultArray, bufferViewCache); break; } attrAccessor.AccessorContent = resultArray; return attrAccessor; } int instancesCount = 0; if (extMeshGPUInstancing.attributes.TryGetValue(EXT_mesh_gpu_instancing.ATTRIBUTE_TRANSLATION, out var positionAccessorId)) { positionsAttr = await GetAttrAccessorAndAccessorContent(positionAccessorId, true); instancesCount = positionsAttr.AccessorContent.AsFloat3s.Length; } if (extMeshGPUInstancing.attributes.TryGetValue(EXT_mesh_gpu_instancing.ATTRIBUTE_ROTATION, out var rotationAccessorId)) { rotationAttr = await GetAttrAccessorAndAccessorContent(rotationAccessorId); if (instancesCount != 0 && rotationAttr.AccessorContent.AsFloat4s.Length != instancesCount) { Debug.LogError("Rotation attribute count does not match position attribute count for instances!", this); return null; } else instancesCount = rotationAttr.AccessorContent.AsFloat4s.Length; } if (extMeshGPUInstancing.attributes.TryGetValue(EXT_mesh_gpu_instancing.ATTRIBUTE_SCALE, out var scaleAccessorId)) { scaleAttr = await GetAttrAccessorAndAccessorContent(scaleAccessorId); if (instancesCount != 0 && scaleAttr.AccessorContent.AsFloat4s.Length != instancesCount) { Debug.LogError("Scale attribute count does not match position attribute count for instances!", this); return null; } else instancesCount = scaleAttr.AccessorContent.AsFloat3s.Length; } if (instancesCount > 0) { List<(Vector3, Quaternion, Vector3)> instancesTRS = new List<(Vector3, Quaternion, Vector3)>(instancesCount); for (int i = 0; i < instancesCount; i++) { instancesTRS.Add(( positionsAttr != null ? positionsAttr.AccessorContent.AsFloat3s[i].ToUnityVector3Raw() : Vector3.zero, rotationAttr != null ? rotationAttr.AccessorContent.AsFloat4s[i].ToUnityQuaternionConvert() : Quaternion.identity, scaleAttr != null ? scaleAttr.AccessorContent.AsFloat3s[i].ToUnityVector3Raw() : Vector3.one )); } return instancesTRS.ToArray(); } } return null; } private bool ShouldBeVisible(Node node, GameObject nodeObj) { if (node.Extensions != null && node.Extensions.TryGetValue(KHR_node_visibility_Factory.EXTENSION_NAME, out var ext)) { return (ext as KHR_node_visibility).visible; } else return true; } protected virtual async Task ConstructNode(Node node, int nodeIndex, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (_assetCache.NodeCache[nodeIndex] != null) { return; } var nodeObj = new GameObject(string.IsNullOrEmpty(node.Name) ? ("GLTFNode" + nodeIndex) : node.Name); // If we're creating a really large node, we need it to not be visible in partial stages. So we hide it while we create it nodeObj.SetActive(false); Vector3 position; Quaternion rotation; Vector3 scale; node.GetUnityTRSProperties(out position, out rotation, out scale); _assetCache.NodeCache[nodeIndex] = nodeObj; nodeObj.transform.localPosition = position; nodeObj.transform.localRotation = rotation; nodeObj.transform.localScale = scale; async Task CreateNodeComponentsAndChilds(bool ignoreMesh = false, bool onlyMesh = false) { // If we're creating a really large node, we need it to not be visible in partial stages. So we hide it while we create it nodeObj.SetActive(false); if (!onlyMesh && node.Children != null) { foreach (var child in node.Children) { GameObject childObj = await GetNode(child.Id, cancellationToken); childObj.transform.SetParent(nodeObj.transform, false); } } if (!ignoreMesh && node.Mesh != null && node.Mesh.Value?.Primitives != null) { var mesh = node.Mesh.Value; await ConstructMesh(mesh, node.Mesh.Id, cancellationToken); var unityMesh = _assetCache.MeshCache[node.Mesh.Id].LoadedMesh; var materials = node.Mesh.Value.Primitives.Select(p => p.Material != null ? _assetCache.MaterialCache[p.Material.Id].UnityMaterialWithVertexColor : _defaultLoadedMaterial.UnityMaterialWithVertexColor ).ToArray(); var morphTargets = mesh.Primitives[0].Targets; var weights = node.Weights ?? mesh.Weights ?? (morphTargets != null ? new List(morphTargets.Select(mt => 0.0)) : null); if (node.Skin != null || weights != null) { var renderer = nodeObj.AddComponent(); renderer.sharedMesh = unityMesh; renderer.sharedMaterials = materials; renderer.quality = SkinQuality.Auto; if (node.Skin != null) await SetupBones(node.Skin.Value, renderer, cancellationToken); // morph target weights if (weights != null) { for (int i = 0; i < weights.Count; ++i) { renderer.SetBlendShapeWeight(i, (float)(weights[i] * _options.BlendShapeFrameWeight)); } } } else { var filter = nodeObj.AddComponent(); filter.sharedMesh = unityMesh; var renderer = nodeObj.AddComponent(); renderer.sharedMaterials = materials; } #if UNITY_PHYSICS if (!onlyMesh) { switch (Collider) { case ColliderType.Box: var boxCollider = nodeObj.AddComponent(); boxCollider.center = unityMesh.bounds.center; boxCollider.size = unityMesh.bounds.size; break; case ColliderType.Mesh: var meshCollider = nodeObj.AddComponent(); meshCollider.sharedMesh = unityMesh; break; case ColliderType.MeshConvex: var meshConvexCollider = nodeObj.AddComponent(); meshConvexCollider.sharedMesh = unityMesh; meshConvexCollider.convex = true; break; } } #endif } if (onlyMesh) { nodeObj.SetActive(ShouldBeVisible(node, nodeObj)); return; } await ConstructLods(_gltfRoot, nodeObj, node, nodeIndex, cancellationToken); var hasLight = ConstructLights(nodeObj, node); var hasCamera = ConstructCamera(nodeObj, node); // Cameras and lights have a different forward axis in glTF vs. Unity. // Thus, when importing lights and cameras we have to flip them. // To ensure children are still oriented correctly, we need to add an inbetween node // That counters the transformation of the parent node. // This way, animations can still correctly apply – e.g. if a camera is animated, // the childs should move along. We can't just add the camera to an empty child and flip that. if ((hasLight || hasCamera) && nodeObj.transform.childCount > 0 && node.Children?.Count > 0) { var flipQuaternion = Quaternion.Inverse(SchemaExtensions.InvertDirection); // Special case for hierarchy simplification and roundtrips: if we have // - exactly one child // - that's flipped 180° // - and doesn't have any components // - and the node doesn't have any extensions // we can just remove that, it's likely an inbetween our own exporter has added. // Theoretically, there are more conditions (not checked here): // - it's not targeted by any animations // - it's not the target of any glTF pointer or index var firstNode = node.Children?.FirstOrDefault()?.Value; var firstChild = nodeObj.transform.GetChild(0); if (nodeObj.transform.childCount == 1 && node.Children?.Count == 1 && (firstNode.Extensions == null || !firstNode.Extensions.Any()) && firstChild.GetComponents().Length == 1 && Quaternion.Angle(firstChild.localRotation, flipQuaternion) < 0.1f) { firstChild.localRotation *= flipQuaternion; var childCount = firstChild.childCount; for (var i = 0; i < childCount; i++) { // Index 0 is correct here, after removing the first child, the next one is now the first and we want to keep the order. var child = firstChild.GetChild(0); child.SetParent(nodeObj.transform, true); } UnityEngine.Object.DestroyImmediate(firstChild.gameObject); } // Otherwise, we need to add an inbetween object else { var childCount = nodeObj.transform.childCount; var inbetween = new GameObject(); inbetween.name = node.Name + "-flipped"; // make sure this objects sits exactly where the nodeObj is inbetween.transform.SetParent(nodeObj.transform, false); inbetween.transform.SetParent(null, true); // move all children to the inbetween object for (int i = 0; i < childCount; i++) { // Index 0 is correct here, after removing the first child, the next one is now the first and we want to keep the order. nodeObj.transform.GetChild(0).SetParent(inbetween.transform, true); } inbetween.transform.SetParent(nodeObj.transform, true); inbetween.transform.localRotation = Quaternion.Inverse(SchemaExtensions.InvertDirection); } } nodeObj.SetActive( ShouldBeVisible(node, nodeObj)); } var instancesTRS = await GetInstancesTRS(node); if (instancesTRS == null || instancesTRS.Length == 0) { await CreateNodeComponentsAndChilds(); } else { var shouldBeVisible = ShouldBeVisible(node, nodeObj); await CreateNodeComponentsAndChilds(true); var instanceParentNode = new GameObject("Instances"); instanceParentNode.transform.SetParent(nodeObj.transform, false); instanceParentNode.gameObject.SetActive(false); GameObject firstInstance = null; for (int i = 0; i < instancesTRS.Length; i++) { if (!firstInstance) { nodeObj = new GameObject(string.IsNullOrEmpty(node.Name) ? ("GLTFNode" + nodeIndex) : node.Name); nodeObj.transform.SetParent(instanceParentNode.transform, false); await CreateNodeComponentsAndChilds(false, true); firstInstance = nodeObj; var renderers = firstInstance.GetComponentsInChildren(); foreach (var renderer in renderers) foreach (var sh in renderer.sharedMaterials) sh.enableInstancing = true; var skinRenderers = firstInstance.GetComponentsInChildren(); foreach (var renderer in skinRenderers) foreach (var sh in renderer.sharedMaterials) sh.enableInstancing = true; } else { nodeObj = GameObject.Instantiate(firstInstance); nodeObj.transform.SetParent(instanceParentNode.transform, false); } nodeObj.transform.localPosition = instancesTRS[i].Item1; nodeObj.transform.localRotation = instancesTRS[i].Item2; nodeObj.transform.localScale = instancesTRS[i].Item3; nodeObj.name = $"Instance {i.ToString()}"; } instanceParentNode.gameObject.SetActive(shouldBeVisible); } progressStatus.NodeLoaded++; progress?.Report(progressStatus); } private async Task ConstructBufferData(Node node, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); MeshId mesh = node.Mesh; if (mesh != null) { if (mesh.Value.Primitives != null) { await ConstructMeshAttributes(mesh.Value, mesh); } } if (node.Children != null) { foreach (NodeId child in node.Children) { await ConstructBufferData(child.Value, cancellationToken); } } } protected async Task ConstructBuffer(GLTFBuffer buffer, int bufferIndex) { if (_assetCache.BufferCache[bufferIndex] != null) return; #if HAVE_MESHOPT_DECOMPRESS if (buffer.Extensions != null && buffer.Extensions.ContainsKey(EXT_meshopt_compression_Factory.EXTENSION_NAME)) { if (_assetCache.BufferCache[bufferIndex] != null) Debug.Log(LogType.Error, $"_assetCache.BufferCache[bufferIndex] != null; (File: {_gltfFileName})"); var bufferCacheDate = new BufferCacheData { bufferData = new NativeArray((int)buffer.ByteLength, Allocator.Persistent), ChunkOffset = 0 }; meshOptNativeBuffers.Add(bufferCacheDate.bufferData); _assetCache.BufferCache[bufferIndex] = bufferCacheDate; return; } #else if (buffer.Extensions != null && buffer.Extensions.ContainsKey(EXT_meshopt_compression_Factory.EXTENSION_NAME)) { //TODO: check for fallback URI or Buffer... ? throw new NotSupportedException($"Can't import model because it uses the EXT_meshopt_compression extension. Add the package \"com.unity.meshopt.decompress\" to your project to import this file. (File: {_gltfFileName})"); } #endif if (buffer.Uri == null) { if (_assetCache.BufferCache[bufferIndex] != null) Debug.Log(LogType.Error, $"Error: _assetCache.BufferCache[bufferIndex] != null. Please report a bug. (File: {_gltfFileName})"); _assetCache.BufferCache[bufferIndex] = ConstructBufferFromGLB(bufferIndex); progressStatus.BuffersLoaded++; progress?.Report(progressStatus); } else { Stream bufferDataStream = null; var uri = buffer.Uri; byte[] bufferData; URIHelper.TryParseBase64(uri, out bufferData); if (bufferData != null) { bufferDataStream = new MemoryStream(bufferData, 0, bufferData.Length, false, true); } else { bufferDataStream = await _options.DataLoader.LoadStreamAsync(buffer.Uri); } if (_assetCache.BufferCache[bufferIndex] != null) Debug.Log(LogType.Error, $"_assetCache.BufferCache[bufferIndex] != null; (File: {_gltfFileName})"); _assetCache.BufferCache[bufferIndex] = new BufferCacheData { Stream = bufferDataStream, bufferData = GetOrCreateNativeBuffer(bufferDataStream) }; progressStatus.BuffersLoaded++; progress?.Report(progressStatus); } } protected virtual async Task ConstructScene(GLTFScene scene, bool showSceneObj, CancellationToken cancellationToken) { if (scene == null) { return; } var sceneObj = new GameObject(string.IsNullOrEmpty(scene.Name) ? ("Scene") : scene.Name); try { sceneObj.SetActive(showSceneObj); if (scene.Nodes != null) { Transform[] nodeTransforms = new Transform[scene.Nodes.Count]; for (int i = 0; i < scene.Nodes.Count; ++i) { NodeId node = scene.Nodes[i]; GameObject nodeObj = await GetNode(node.Id, cancellationToken); nodeObj.transform.SetParent(sceneObj.transform, false); nodeTransforms[i] = nodeObj.transform; } } if (_options.AnimationMethod != AnimationMethod.None) { if (_gltfRoot.Animations != null && _gltfRoot.Animations.Count > 0) { #if UNITY_ANIMATION || !UNITY_2019_1_OR_NEWER // create the AnimationClip that will contain animation data var constructedClips = new List(); for (int i = 0; i < _gltfRoot.Animations.Count; ++i) { AnimationClip clip = await ConstructClip(sceneObj.transform, i, cancellationToken); clip.wrapMode = WrapMode.Loop; #if UNITY_EDITOR var settings = UnityEditor.AnimationUtility.GetAnimationClipSettings(clip); settings.loopTime = _options.AnimationLoopTime; settings.loopBlend = _options.AnimationLoopPose; UnityEditor.AnimationUtility.SetAnimationClipSettings(clip, settings); #endif constructedClips.Add(clip); } if (_options.AnimationMethod == AnimationMethod.Legacy) { Animation animation = sceneObj.AddComponent(); for (int i = 0; i < constructedClips.Count; i++) { var clip = constructedClips[i]; clip.wrapMode = _options.AnimationLoopTime ? WrapMode.Loop : WrapMode.Default; animation.AddClip(clip, clip.name); if (i == 0) { animation.clip = clip; } } } else if (_options.AnimationMethod == AnimationMethod.Mecanim || _options.AnimationMethod == AnimationMethod.MecanimHumanoid) { Animator animator = sceneObj.AddComponent(); #if UNITY_EDITOR // TODO there's no good way to construct an AnimatorController on import it seems, needs to be a SubAsset etc. var controller = new UnityEditor.Animations.AnimatorController(); controller.name = "AnimatorController"; controller.AddLayer("Base Layer"); var baseLayer = controller.layers[0]; for (int i = 0; i < constructedClips.Count; i++) { var name = constructedClips[i].name; // can't be empty if (string.IsNullOrWhiteSpace(name)) name = "clip " + i; // can't contain ., / and \ name = name.Replace(".", "_"); name = name.Replace("/", "_"); name = name.Replace("\\", "_"); var state = baseLayer.stateMachine.AddState(name); state.motion = constructedClips[i]; } animator.runtimeAnimatorController = controller; #else Debug.Log(LogType.Warning, "Importing animations at runtime requires the Legacy AnimationMethod to be enabled, or custom handling of the resulting clips."); #endif } #else Debug.Log(LogType.Warning, "glTF scene contains animations but com.unity.modules.animation isn't installed. Install that module to import animations."); #endif if (AnyAnimationTimeNotIncreasing) { Debug.Log(LogType.Warning, $"Time of some subsequent animation keyframes is not increasing in {_gltfFileName} (glTF-Validator error ACCESSOR_ANIMATION_INPUT_NON_INCREASING)"); } CreatedAnimationClips = constructedClips.ToArray(); } } if (_options.AnimationMethod == AnimationMethod.MecanimHumanoid) { if (!sceneObj.GetComponent()) sceneObj.AddComponent(); } CreatedObject = sceneObj; InitializeGltfTopLevelObject(); } catch (Exception ex) { // If some failure occured during loading, clean up the scene UnityEngine.Object.DestroyImmediate(sceneObj); CreatedObject = null; if (ex is OutOfMemoryException) { #if UNITY_2023_1_OR_NEWER await #endif Resources.UnloadUnusedAssets(); } throw; } } protected virtual BufferCacheData ConstructBufferFromGLB(int bufferIndex) { GLTFParser.SeekToBinaryChunk(_gltfStream.Stream, bufferIndex, _gltfStream.StartPosition); // sets stream to correct start position return new BufferCacheData { Stream = _gltfStream.Stream, ChunkOffset = (uint)_gltfStream.Stream.Position, bufferData = GetOrCreateNativeBuffer(_gltfStream.Stream), }; } /// /// Get the absolute path to a gltf uri reference. /// /// The path to the gltf file /// A path without the filename or extension protected static string AbsoluteUriPath(string gltfPath) { var uri = new Uri(gltfPath); var partialPath = uri.AbsoluteUri.Remove(uri.AbsoluteUri.Length - uri.Query.Length - uri.Segments[uri.Segments.Length - 1].Length); return partialPath; } /// /// Get the absolute path a gltf file directory /// /// The path to the gltf file /// A path without the filename or extension protected static string AbsoluteFilePath(string gltfPath) { var fileName = Path.GetFileName(gltfPath); var lastIndex = gltfPath.IndexOf(fileName); var partialPath = gltfPath.Substring(0, lastIndex); return partialPath; } /// /// Cleans up any undisposed streams after loading a scene or a node. /// private void Cleanup() { if (_assetCache != null) { _assetCache.Dispose(); _assetCache = null; } } private void DisposeNativeBuffers() { foreach (var buffer in _nativeBuffers) { if (buffer.Value.IsCreated) buffer.Value.Dispose(); } _nativeBuffers.Clear(); #if HAVE_MESHOPT_DECOMPRESS foreach (var meshOptBuffer in meshOptNativeBuffers) { meshOptBuffer.Dispose(); } meshOptNativeBuffers.Clear(); #endif } private async Task SetupLoad(Func callback) { try { lock (this) { if (_isRunning) { throw new GLTFLoadException($"Cannot start a load while GLTFSceneImporter is already running (File: {_gltfFileName})"); } _isRunning = true; } Statistics = new ImportStatistics(); if (_options.ThrowOnLowMemory) { _memoryChecker = new MemoryChecker(); } if (_gltfRoot == null) { await LoadJson(_gltfFileName); } if (_assetCache == null) { _assetCache = new AssetCache(_gltfRoot); } await callback(); } catch { Cleanup(); throw; } finally { lock (this) { _isRunning = false; } _gltfStream.Stream.Close(); DisposeNativeBuffers(); } } protected async Task YieldOnTimeoutAndThrowOnLowMemory() { if (_options.ThrowOnLowMemory) { _memoryChecker.ThrowIfOutOfMemory(); } if (_options.AsyncCoroutineHelper != null) { await _options.AsyncCoroutineHelper.YieldOnTimeout(); } } } }