#pragma warning disable 168, 618 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using TriLibCore.Extensions; using TriLibCore.General; using TriLibCore.Interfaces; using TriLibCore.Mappers; using TriLibCore.Textures; using TriLibCore.Utils; using UnityEngine; using FileMode = System.IO.FileMode; using HumanDescription = UnityEngine.HumanDescription; using Object = UnityEngine.Object; using TriLibCore.Attributes; using TriLibCore.Collections; using TriLibCore.Geometries; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; #if TRILIB_DRACO using TriLibCore.Gltf.Reader; using TriLibCore.Gltf.Draco; #endif #if UNITY_EDITOR using UnityEditor; #endif namespace TriLibCore { /// Represents the main class containing methods to load the Models. public static partial class AssetLoader { /// /// Asset Loader Options validation message. /// private const string ValidationMessage = "You can disable these validations in the Edit->Project Settings->TriLib menu."; /// /// Constant that defines the namespace used by TriLib Mappers. /// private const string TriLibMappersNamespace = "TriLibCore.Mappers"; /// Loads a Model from the given path asynchronously. /// The Model file path. /// The Method to call on the Main Thread when the Model is loaded but resources may still pending. /// The Method to call on the Main Thread when the Model and resources are loaded. /// The Method to call when the Model loading progress changes. /// The Method to call on the Main Thread when any error occurs. /// The Game Object that will be the parent of the loaded Game Object. Can be null. /// The options to use when loading the Model. /// The Custom Data that will be passed along the Context. /// Turn on this field to avoid loading the model immediately and chain the Tasks. /// The method to call on the parallel Thread before the Unity objects are created. /// Indicates whether to load from a Zip file. /// The Asset Loader Context, containing Model loading information and the output Game Object. public static AssetLoaderContext LoadModelFromFile(string path, Action onLoad = null, Action onMaterialsLoad = null, Action onProgress = null, Action onError = null, GameObject wrapperGameObject = null, AssetLoaderOptions assetLoaderOptions = null, object customContextData = null, bool haltTask = false, Action onPreLoad = null, bool isZipFile = false) { var assetLoaderContext = new AssetLoaderContext { Options = assetLoaderOptions ?? CreateDefaultLoaderOptions(), Filename = path, BasePath = FileUtils.GetFileDirectory(path), WrapperGameObject = wrapperGameObject, OnMaterialsLoad = onMaterialsLoad, OnLoad = onLoad, OnProgress = onProgress, HandleError = HandleError, OnError = onError, OnPreLoad = onPreLoad, CustomData = customContextData, HaltTasks = haltTask, #if (UNITY_WEBGL && !TRILIB_ENABLE_WEBGL_THREADS) || (UNITY_WSA && !TRILIB_ENABLE_UWP_THREADS) || TRILIB_FORCE_SYNC Async = false, #else Async = true, #endif IsZipFile = isZipFile, PersistentDataPath = Application.persistentDataPath }; LoadModelInternal(assetLoaderContext); return assetLoaderContext; } /// Loads a Model from the given Stream asynchronously. /// The Stream containing the Model data. /// The Model filename. /// The Model file extension. (Eg.: fbx) /// The Method to call on the Main Thread when the Model is loaded but resources may still pending. /// The Method to call on the Main Thread when the Model and resources are loaded. /// The Method to call when the Model loading progress changes. /// The Method to call on the Main Thread when any error occurs. /// The Game Object that will be the parent of the loaded Game Object. Can be null. /// The options to use when loading the Model. /// The Custom Data that will be passed along the Context. /// Turn on this field to avoid loading the model immediately and chain the Tasks. /// The method to call on the parallel Thread before the Unity objects are created. /// Indicates whether to load from a Zip file. /// The Asset Loader Context, containing Model loading information and the output Game Object. public static AssetLoaderContext LoadModelFromStream(Stream stream, string filename = null, string fileExtension = null, Action onLoad = null, Action onMaterialsLoad = null, Action onProgress = null, Action onError = null, GameObject wrapperGameObject = null, AssetLoaderOptions assetLoaderOptions = null, object customContextData = null, bool haltTask = false, Action onPreLoad = null, bool isZipFile = false) { var assetLoaderContext = new AssetLoaderContext { Options = assetLoaderOptions ?? CreateDefaultLoaderOptions(), Stream = stream, Filename = filename, FileExtension = fileExtension ?? FileUtils.GetFileExtension(filename, false), BasePath = FileUtils.GetFileDirectory(filename), WrapperGameObject = wrapperGameObject, OnMaterialsLoad = onMaterialsLoad, OnLoad = onLoad, OnProgress = onProgress, HandleError = HandleError, OnError = onError, OnPreLoad = onPreLoad, CustomData = customContextData, HaltTasks = haltTask, #if (UNITY_WEBGL && !TRILIB_ENABLE_WEBGL_THREADS) || (UNITY_WSA && !TRILIB_ENABLE_UWP_THREADS) || TRILIB_FORCE_SYNC Async = false, #else Async = true, #endif IsZipFile = isZipFile, PersistentDataPath = Application.persistentDataPath }; LoadModelInternal(assetLoaderContext); return assetLoaderContext; } /// Loads a Model from the given path synchronously. /// The Model file path. /// The Method to call on the Main Thread when any error occurs. /// The Game Object that will be the parent of the loaded Game Object. Can be null. /// The options to use when loading the Model. /// The Custom Data that will be passed along the Context. /// Indicates whether to load from a Zip file. /// The Asset Loader Context, containing Model loading information and the output Game Object. public static AssetLoaderContext LoadModelFromFileNoThread(string path, Action onError = null, GameObject wrapperGameObject = null, AssetLoaderOptions assetLoaderOptions = null, object customContextData = null, bool isZipFile = false) { var assetLoaderContext = new AssetLoaderContext { Options = assetLoaderOptions ?? CreateDefaultLoaderOptions(), Filename = path, BasePath = FileUtils.GetFileDirectory(path), CustomData = customContextData, HandleError = HandleError, OnError = onError, WrapperGameObject = wrapperGameObject, Async = false, IsZipFile = isZipFile, PersistentDataPath = Application.persistentDataPath }; LoadModelInternal(assetLoaderContext); return assetLoaderContext; } /// Loads a Model from the given Stream synchronously. /// The Stream containing the Model data. /// The Model filename. /// The Model file extension. (Eg.: fbx) /// The Method to call on the Main Thread when any error occurs. /// The Game Object that will be the parent of the loaded Game Object. Can be null. /// The options to use when loading the Model. /// The Custom Data that will be passed along the Context. /// Indicates whether to load from a Zip file. /// The Asset Loader Context, containing Model loading information and the output Game Object. public static AssetLoaderContext LoadModelFromStreamNoThread(Stream stream, string filename = null, string fileExtension = null, Action onError = null, GameObject wrapperGameObject = null, AssetLoaderOptions assetLoaderOptions = null, object customContextData = null, bool isZipFile = false) { var assetLoaderContext = new AssetLoaderContext { Options = assetLoaderOptions ?? CreateDefaultLoaderOptions(), Stream = stream, Filename = filename, FileExtension = fileExtension ?? FileUtils.GetFileExtension(filename, false), BasePath = FileUtils.GetFileDirectory(filename), CustomData = customContextData, HandleError = HandleError, OnError = onError, WrapperGameObject = wrapperGameObject, Async = false, IsZipFile = isZipFile, PersistentDataPath = Application.persistentDataPath }; LoadModelInternal(assetLoaderContext); return assetLoaderContext; } /// Begins the model loading process. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void LoadModelInternal(AssetLoaderContext assetLoaderContext) { SetupCallbacks(); #if !TRILIB_DISABLE_VALIDATIONS ValidateAssetLoaderOptions(assetLoaderContext.Options); #endif #if TRILIB_USE_THREAD_NAMES && !UNITY_WSA var threadName = "TriLib_LoadModelFromStream"; #else string threadName = null; #endif var fileExtension = assetLoaderContext.FileExtension; if (fileExtension == null && assetLoaderContext.Filename != null) { fileExtension = FileUtils.GetFileExtension(assetLoaderContext.Filename, false); } if (fileExtension == "zip") { AssetLoaderZip.LoadModelFromZipFile(assetLoaderContext.Filename, assetLoaderContext.OnLoad, assetLoaderContext.OnMaterialsLoad, assetLoaderContext.OnProgress, assetLoaderContext.OnError, assetLoaderContext.WrapperGameObject, assetLoaderContext.Options, assetLoaderContext.CustomData, assetLoaderContext.FileExtension, assetLoaderContext.HaltTasks, assetLoaderContext.OnPreLoad); } else { ThreadUtils.RequestNewThreadFor( assetLoaderContext, LoadModel, ProcessRootModel, HandleError, assetLoaderContext.Options.Timeout, threadName, !assetLoaderContext.HaltTasks, assetLoaderContext.OnPreLoad ); } } /// /// Configures callbacks. /// private static void SetupCallbacks() { Texture.allowThreadedTextureCreation = true; MaterialMapper.CreateTextureCallback = TextureLoaders.CreateTexture; MaterialMapper.ScanForAlphaPixelsCallback = TextureLoaders.ScanForAlphaPixels; MaterialMapper.LoadTextureCallback = TextureLoaders.LoadTexture; MaterialMapper.PostProcessTextureCallback = TextureLoaders.PostProcessTexture; MaterialMapper.ApplyTextureCallback = TextureUtils.ApplyTexture2D; MaterialMapper.FixNPOTTextureCallback = TextureUtils.FixNPOTTexture; MaterialMapper.FixNormalMapCallback = TextureUtils.FixNormalMap; } /// /// Validates the given AssetLoaderOptions. /// /// The options to use when loading the Model. private static void ValidateAssetLoaderOptions(AssetLoaderOptions assetLoaderOptions) { #if ENABLE_IL2CPP if (assetLoaderOptions.EnableProfiler) { assetLoaderOptions.EnableProfiler = false; Debug.LogWarning("TriLib: The built in profiler has been disabled as it does not work with IL2CPP builds."); Debug.LogWarning(ValidationMessage); } #endif if (GraphicsSettingsUtils.IsUsingUniversalPipeline && assetLoaderOptions.LoadTexturesAsSRGB) { Debug.LogWarning("TriLib: Textures must be loaded as Linear on the UniversalRP."); Debug.LogWarning(ValidationMessage); assetLoaderOptions.LoadTexturesAsSRGB = false; } } #if UNITY_EDITOR private static Object LoadOrCreateScriptableObject(string type, string @namespace, string subFolder) { string mappersFilePath; var triLibMapperAssets = AssetDatabase.FindAssets("TriLibMappersPlaceholder"); if (triLibMapperAssets.Length > 0) { mappersFilePath = AssetDatabase.GUIDToAssetPath(triLibMapperAssets[0]); } else { throw new Exception("Could not find \"TriLibMappersPlaceholder\" file. Please re-import TriLib package."); } var mappersDirectory = $"{FileUtils.GetFileDirectory(mappersFilePath)}"; var assetDirectory = $"{mappersDirectory}/{subFolder}"; if (!AssetDatabase.IsValidFolder(assetDirectory)) { AssetDatabase.CreateFolder(mappersDirectory, subFolder); } var assetPath = $"{assetDirectory}/{type}.asset"; var scriptableObject = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Object)); if (scriptableObject == null) { scriptableObject = CreateScriptableObjectSafe(type, @namespace); if (scriptableObject != null) { AssetDatabase.CreateAsset(scriptableObject, assetPath); } } return scriptableObject; } #endif /// Creates an Asset Loader Options with the default settings and Mappers. /// Indicates whether created Scriptable Objects will be saved as assets. /// Pass `true `if you are caching your AssetLoaderOptions instance. /// The Asset Loader Options containing the default settings. public static AssetLoaderOptions CreateDefaultLoaderOptions(bool generateAssets = false, bool supressWarning = false) { if (!supressWarning) { Debug.LogWarning("TriLib: You are creating a new AssetLoaderOptions instance. If you are caching this instance and don't want this message to be displayed again, pass `false` to the `supressWarning` parameter of `CreateDefaultLoaderOptions` call."); } var assetLoaderOptions = ScriptableObject.CreateInstance(); ByBonesRootBoneMapper byBonesRootBoneMapper; #if UNITY_EDITOR if (generateAssets) { byBonesRootBoneMapper = (ByBonesRootBoneMapper)LoadOrCreateScriptableObject("ByBonesRootBoneMapper", TriLibMappersNamespace, "RootBone"); } else { byBonesRootBoneMapper = ScriptableObject.CreateInstance(); } #else byBonesRootBoneMapper = ScriptableObject.CreateInstance(); #endif byBonesRootBoneMapper.name = "ByBonesRootBoneMapper"; assetLoaderOptions.RootBoneMapper = byBonesRootBoneMapper; var materialMappers = new List(); for (var i = 0; i < MaterialMapper.RegisteredMappers.Count; i++) { var materialMapperName = MaterialMapper.RegisteredMappers[i]; var materialMapperNamespace = MaterialMapper.RegisteredMapperNamespaces[i]; if (materialMapperName == null) { continue; } MaterialMapper materialMapper; try { #if UNITY_EDITOR if (generateAssets) { materialMapper = LoadOrCreateScriptableObject(materialMapperName, materialMapperNamespace, "Material") as MaterialMapper; } else { materialMapper = CreateScriptableObjectSafe(materialMapperName, materialMapperNamespace) as MaterialMapper; } #else materialMapper = CreateScriptableObjectSafe(materialMapperName, materialMapperNamespace) as MaterialMapper; #endif } catch { materialMapper = null; } if (materialMapper is object) { materialMapper.name = materialMapperName; if (materialMapper.IsCompatible(null)) { materialMappers.Add(materialMapper); } else { #if UNITY_EDITOR var assetPath = AssetDatabase.GetAssetPath(materialMapper); if (assetPath == null) { Object.DestroyImmediate(materialMapper); } #else Object.Destroy(materialMapper); #endif } } } if (materialMappers.Count == 0) { Debug.LogWarning("TriLib could not find any suitable MaterialMapper on the project."); } else { assetLoaderOptions.MaterialMappers = materialMappers.ToArray(); } return assetLoaderOptions; } /// /// Tries to create a ScriptableObject with the given parameters, without throwing an internal Exception. /// /// The ScriptableObject type name. /// The ScriptableObject type namespace. /// The created ScriptableObject, or null. private static ScriptableObject CreateScriptableObjectSafe(string typeName, string @namespace) { var type = System.Type.GetType($"{@namespace}.{typeName}"); return type != null ? ScriptableObject.CreateInstance(typeName) : null; } /// Processes the Model from the given context and begin to build the Game Objects. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void ProcessModel(AssetLoaderContext assetLoaderContext) { if (assetLoaderContext.RootModel != null) { CreateModel(assetLoaderContext, assetLoaderContext.WrapperGameObject != null ? assetLoaderContext.WrapperGameObject.transform : null, assetLoaderContext.RootModel, assetLoaderContext.RootModel, true); if (assetLoaderContext.RootGameObject.transform.localScale.sqrMagnitude == 0f) { assetLoaderContext.RootGameObject.transform.localScale = Vector3.one; } SetupModelLod(assetLoaderContext, assetLoaderContext.RootModel); if (assetLoaderContext.Options.AnimationType != AnimationType.None || assetLoaderContext.Options.ImportBlendShapes) { SetupModelBones(assetLoaderContext, assetLoaderContext.RootModel); BuildGameObjectsPaths(assetLoaderContext); SetupRig(assetLoaderContext); } assetLoaderContext.RootGameObject.isStatic = assetLoaderContext.Options.Static; } assetLoaderContext.OnLoad?.Invoke(assetLoaderContext); } /// Configures the context Model LODs (levels-of-detail) if there are any. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Model containing the LOD data. private static void SetupModelLod(AssetLoaderContext assetLoaderContext, IModel model) { if (model.Children != null && model.Children.Count > 0) { var lodModels = new Dictionary>(model.Children.Count); var minLod = int.MaxValue; var maxLod = 0; for (var i = 0; i < model.Children.Count; i++) { var child = model.Children[i]; var match = Regex.Match(child.Name, "_LOD(?[0-9]+)|LOD_(?[0-9]+)"); if (match.Success) { var lodNumber = Convert.ToInt32(match.Groups["number"].Value); minLod = Mathf.Min(lodNumber, minLod); maxLod = Mathf.Max(lodNumber, maxLod); if (!lodModels.TryGetValue(lodNumber, out var renderers)) { renderers = new List(); lodModels.Add(lodNumber, renderers); } renderers.AddRange(assetLoaderContext.GameObjects[child].GetComponentsInChildren()); } } if (lodModels.Count > 1) { var newGameObject = assetLoaderContext.GameObjects[model]; var lods = new List(lodModels.Count + 1); var lodGroup = newGameObject.AddComponent(); var lastPosition = assetLoaderContext.Options.LODScreenRelativeTransitionHeightBase; for (var i = minLod; i <= maxLod; i++) { if (lodModels.TryGetValue(i, out var renderers)) { lods.Add(new LOD(lastPosition, renderers.ToArray())); lastPosition *= 0.5f; } } lodGroup.SetLODs(lods.ToArray()); } for (var i = 0; i < model.Children.Count; i++) { var child = model.Children[i]; SetupModelLod(assetLoaderContext, child); } } } /// Builds the Game Object Converts hierarchy paths. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void BuildGameObjectsPaths(AssetLoaderContext assetLoaderContext) { foreach (var value in assetLoaderContext.GameObjects.Values) { assetLoaderContext.GameObjectPaths.Add(value, value.transform.BuildPath(assetLoaderContext.RootGameObject.transform)); } } /// Configures the context Model rigging if there is any. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void SetupRig(AssetLoaderContext assetLoaderContext) { var animations = assetLoaderContext.RootModel.AllAnimations; AnimationClip[] animationClips = null; if (assetLoaderContext.Options.AnimationType == AnimationType.Humanoid || animations != null && animations.Count > 0) { switch (assetLoaderContext.Options.AnimationType) { case AnimationType.Legacy: { SetupAnimationComponents(assetLoaderContext, animations, out animationClips, out var animator, out var unityAnimation); break; } case AnimationType.Generic: { SetupAnimationComponents(assetLoaderContext, animations, out animationClips, out var animator, out var unityAnimation); if (assetLoaderContext.Options.AvatarDefinition == AvatarDefinitionType.CopyFromOtherAvatar) { animator.avatar = assetLoaderContext.Options.Avatar; } else { SetupGenericAvatar(assetLoaderContext, animator); } break; } case AnimationType.Humanoid: { SetupAnimationComponents(assetLoaderContext, animations, out animationClips, out var animator, out var unityAnimation); if (assetLoaderContext.Options.AvatarDefinition == AvatarDefinitionType.CopyFromOtherAvatar) { animator.avatar = assetLoaderContext.Options.Avatar; } else if (assetLoaderContext.Options.HumanoidAvatarMapper != null) { SetupHumanoidAvatar(assetLoaderContext, animator); } break; } } if (animationClips != null) { if (assetLoaderContext.Options.AnimationClipMappers != null) { Array.Sort(assetLoaderContext.Options.AnimationClipMappers, (a, b) => a.CheckingOrder > b.CheckingOrder ? -1 : 1); foreach (var animationClipMapper in assetLoaderContext.Options.AnimationClipMappers) { animationClips = animationClipMapper.MapArray(assetLoaderContext, animationClips); if (animationClips != null && animationClips.Length > 0) { break; } } } for (var i = 0; i < animationClips.Length; i++) { var animationClip = animationClips[i]; assetLoaderContext.Allocations.Add(animationClip); } } } } /// /// Creates animation components for the given context. /// /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Animations loaded for the Model. /// The AnimationClips that will be created for the Model. /// The Animator that will be created for the Model. /// The Animation Component that will be created for the Model. private static void SetupAnimationComponents(AssetLoaderContext assetLoaderContext, IList animations, out AnimationClip[] animationClips, out Animator animator, out Animation unityAnimation) { if (assetLoaderContext.Options.AnimationType == AnimationType.Legacy && assetLoaderContext.Options.EnforceAnimatorWithLegacyAnimations || assetLoaderContext.Options.AnimationType != AnimationType.Legacy) { animator = assetLoaderContext.RootGameObject.AddComponent(); } else { animator = null; } unityAnimation = assetLoaderContext.RootGameObject.AddComponent(); unityAnimation.playAutomatically = assetLoaderContext.Options.AutomaticallyPlayLegacyAnimations; unityAnimation.wrapMode = assetLoaderContext.Options.AnimationWrapMode; if (animations != null) { animationClips = new AnimationClip[animations.Count]; for (var i = 0; i < animations.Count; i++) { var triLibAnimation = animations[i]; var animationClip = CreateAnimation(assetLoaderContext, triLibAnimation); unityAnimation.AddClip(animationClip, animationClip.name); animationClips[i] = animationClip; assetLoaderContext.Reader.UpdateLoadingPercentage(i, assetLoaderContext.Reader.LoadingStepsCount + (int)ReaderBase.PostLoadingSteps.PostProcessAnimationClips, animations.Count); } if (assetLoaderContext.Options.AutomaticallyPlayLegacyAnimations && animationClips.Length > 0) { unityAnimation.clip = animationClips[0]; unityAnimation.Play(animationClips[0].name); } } else { animationClips = null; } } /// Creates a Skeleton Bone for the given Transform. /// The bone Transform to use on the Skeleton Bone. /// The created Skeleton Bone. private static SkeletonBone CreateSkeletonBone(Transform boneTransform) { var skeletonBone = new SkeletonBone { name = boneTransform.name, position = boneTransform.localPosition, rotation = boneTransform.localRotation, scale = boneTransform.localScale }; return skeletonBone; } /// Creates a Human Bone for the given Bone Mapping, containing the relationship between the Transform and Bone. /// The Bone Mapping used to create the Human Bone, containing the information used to search for bones. /// The bone name to use on the created Human Bone. /// The created Human Bone. private static HumanBone CreateHumanBone(BoneMapping boneMapping, string boneName) { var humanBone = new HumanBone { boneName = boneName, humanName = GetHumanBodyName(boneMapping.HumanBone), limit = { useDefaultValues = boneMapping.HumanLimit.useDefaultValues, axisLength = boneMapping.HumanLimit.axisLength, center = boneMapping.HumanLimit.center, max = boneMapping.HumanLimit.max, min = boneMapping.HumanLimit.min } }; return humanBone; } /// Returns the given Human Body Bones name as String. /// The Human Body Bones to get the name from. /// The Human Body Bones name. private static string GetHumanBodyName(HumanBodyBones humanBodyBones) { return HumanTrait.BoneName[(int)humanBodyBones]; } /// Creates a Generic Avatar to the given context Model. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Animator assigned to the given Context Root Game Object. private static void SetupGenericAvatar(AssetLoaderContext assetLoaderContext, Animator animator) { var parent = assetLoaderContext.RootGameObject.transform.parent; assetLoaderContext.RootGameObject.transform.SetParent(null, true); var bones = new List(); assetLoaderContext.RootModel.GetBones(assetLoaderContext, bones); var rootBone = assetLoaderContext.Options.RootBoneMapper.Map(assetLoaderContext, bones); var avatar = AvatarBuilder.BuildGenericAvatar(assetLoaderContext.RootGameObject, rootBone != null ? rootBone.name : ""); avatar.name = $"{assetLoaderContext.RootGameObject.name}Avatar"; animator.avatar = avatar; assetLoaderContext.RootGameObject.transform.SetParent(parent, true); } /// Creates a Humanoid Avatar to the given context Model. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Animator assigned to the given Context Root Game Object. private static void SetupHumanoidAvatar(AssetLoaderContext assetLoaderContext, Animator animator) { var valid = false; var mapping = assetLoaderContext.Options.HumanoidAvatarMapper.Map(assetLoaderContext); if (mapping.Count > 0) { var parent = assetLoaderContext.RootGameObject.transform.parent; var rootGameObjectPosition = assetLoaderContext.RootGameObject.transform.position; assetLoaderContext.RootGameObject.transform.SetParent(null, false); assetLoaderContext.Options.HumanoidAvatarMapper.PostSetup(assetLoaderContext, mapping); Transform hipsTransform = null; var humanBones = new HumanBone[mapping.Count]; var boneIndex = 0; foreach (var kvp in mapping) { if (kvp.Key.HumanBone == HumanBodyBones.Hips) { hipsTransform = kvp.Value; } humanBones[boneIndex++] = CreateHumanBone(kvp.Key, kvp.Value.name); } if (hipsTransform != null) { var skeletonBones = new Dictionary(); var bounds = assetLoaderContext.RootGameObject.CalculateBounds(); var toBottom = bounds.min.y; if (toBottom < 0f) { var hipsTransformPosition = hipsTransform.position; hipsTransformPosition.y -= toBottom; hipsTransform.position = hipsTransformPosition; } var toCenter = Vector3.zero - bounds.center; toCenter.y = 0f; if (toCenter.sqrMagnitude > 0.01f) { var hipsTransformPosition = hipsTransform.position; hipsTransformPosition += toCenter; hipsTransform.position = hipsTransformPosition; } foreach (var kvp in assetLoaderContext.GameObjects) { if (!skeletonBones.ContainsKey(kvp.Value.transform)) { skeletonBones.Add(kvp.Value.transform, CreateSkeletonBone(kvp.Value.transform)); } } var triLibHumanDescription = assetLoaderContext.Options.HumanDescription ?? new General.HumanDescription(); var humanDescription = new HumanDescription { armStretch = triLibHumanDescription.armStretch, feetSpacing = triLibHumanDescription.feetSpacing, hasTranslationDoF = triLibHumanDescription.hasTranslationDof, legStretch = triLibHumanDescription.legStretch, lowerArmTwist = triLibHumanDescription.lowerArmTwist, lowerLegTwist = triLibHumanDescription.lowerLegTwist, upperArmTwist = triLibHumanDescription.upperArmTwist, upperLegTwist = triLibHumanDescription.upperLegTwist, skeleton = skeletonBones.Values.ToArray(), human = humanBones }; var avatar = AvatarBuilder.BuildHumanAvatar(assetLoaderContext.RootGameObject, humanDescription); avatar.name = $"{assetLoaderContext.RootGameObject.name}Avatar"; animator.avatar = avatar; } assetLoaderContext.RootGameObject.transform.SetParent(parent, false); assetLoaderContext.RootGameObject.transform.position = rootGameObjectPosition; valid = animator.avatar.isValid || !assetLoaderContext.Options.ShowLoadingWarnings; } if (!valid) { Debug.LogWarning($"Could not create an Avatar for the model \"{(assetLoaderContext.Filename == null ? "Unknown" : FileUtils.GetShortFilename(assetLoaderContext.Filename))}\""); } } /// Converts the given Model into a Game Object. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The parent Game Object Transform. /// The root Model. /// The Model to convert. /// Is this the first node in the Model hierarchy? private static void CreateModel(AssetLoaderContext assetLoaderContext, Transform parentTransform, IRootModel rootModel, IModel model, bool isRootGameObject) { var newGameObject = new GameObject(model.Name); assetLoaderContext.GameObjects.Add(model, newGameObject); assetLoaderContext.Models.Add(newGameObject, model); newGameObject.transform.parent = parentTransform; newGameObject.transform.localPosition = model.LocalPosition; newGameObject.transform.localRotation = model.LocalRotation; newGameObject.transform.localScale = model.LocalScale; if (model.GeometryGroup != null) { CreateGeometry(assetLoaderContext, newGameObject, rootModel, model); } if (assetLoaderContext.Options.ImportCameras && model is ICamera camera) { CreateCamera(assetLoaderContext, camera, newGameObject); } if (assetLoaderContext.Options.ImportLights && model is ILight light) { CreateLight(assetLoaderContext, light, newGameObject); } if (model.Children != null && model.Children.Count > 0) { for (var i = 0; i < model.Children.Count; i++) { var child = model.Children[i]; CreateModel(assetLoaderContext, newGameObject.transform, rootModel, child, false); } } if (assetLoaderContext.Options.UserPropertiesMapper != null && model.UserProperties != null) { foreach (var userProperty in model.UserProperties) { assetLoaderContext.Options.UserPropertiesMapper.OnProcessUserData(assetLoaderContext, newGameObject, userProperty.Key, userProperty.Value); } } if (isRootGameObject) { assetLoaderContext.RootGameObject = newGameObject; } } /// Converts the given model light, if present into a Light. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Model. /// The Model Game Object. private static void CreateLight(AssetLoaderContext assetLoaderContext, ILight light, GameObject newGameObject) { var unityLight = newGameObject.AddComponent(); unityLight.color = light.Color; unityLight.innerSpotAngle = light.InnerSpotAngle; unityLight.spotAngle = light.OuterSpotAngle; unityLight.intensity = light.Intensity; unityLight.range = light.Range; unityLight.type = light.LightType; unityLight.shadows = light.CastShadows ? LightShadows.Soft : LightShadows.None; #if UNITY_EDITOR unityLight.areaSize = new Vector2(light.Width, light.Height); #endif } /// Converts the given model camera, if present into a Camera. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Model. /// The Model Game Object. private static void CreateCamera(AssetLoaderContext assetLoaderContext, ICamera camera, GameObject newGameObject) { var unityCamera = newGameObject.AddComponent(); unityCamera.aspect = camera.AspectRatio; unityCamera.orthographic = camera.Ortographic; unityCamera.orthographicSize = camera.OrtographicSize; unityCamera.fieldOfView = camera.FieldOfView; unityCamera.nearClipPlane = camera.NearClipPlane; unityCamera.farClipPlane = camera.FarClipPlane; unityCamera.focalLength = camera.FocalLength; unityCamera.sensorSize = camera.SensorSize; unityCamera.lensShift = camera.LensShift; unityCamera.gateFit = camera.GateFitMode; unityCamera.usePhysicalProperties = camera.PhysicalCamera; unityCamera.enabled = true; } /// Configures the given Model skinning if there is any. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Model containing the bones. private static void SetupModelBones(AssetLoaderContext assetLoaderContext, IModel model) { var loadedGameObject = assetLoaderContext.GameObjects[model]; var skinnedMeshRenderer = loadedGameObject.GetComponent(); if (skinnedMeshRenderer != null) { var bones = model.Bones; if (bones != null && bones.Count > 0) { var boneIndex = 0; var gameObjectBones = skinnedMeshRenderer.bones; for (var i = 0; i < bones.Count; i++) { var bone = bones[i]; gameObjectBones[boneIndex++] = assetLoaderContext.GameObjects[bone].transform; } skinnedMeshRenderer.bones = gameObjectBones; skinnedMeshRenderer.rootBone = assetLoaderContext.Options.RootBoneMapper.Map(assetLoaderContext, gameObjectBones); } } if (model.Children != null && model.Children.Count > 0) { for (var i = 0; i < model.Children.Count; i++) { var subModel = model.Children[i]; SetupModelBones(assetLoaderContext, subModel); } } } /// Converts the given Animation into an Animation Clip. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Animation to convert. /// The converted Animation Clip. private static AnimationClip CreateAnimation(AssetLoaderContext assetLoaderContext, IAnimation animation) { var animationClip = new AnimationClip { name = animation.Name, legacy = true, frameRate = animation.FrameRate }; var animationCurveBindings = animation.AnimationCurveBindings; if (animationCurveBindings == null) { return animationClip; } for (var i = animationCurveBindings.Count - 1; i >= 0; i--) { var animationCurveBinding = animationCurveBindings[i]; var animationCurves = animationCurveBinding.AnimationCurves; if (!assetLoaderContext.GameObjects.ContainsKey(animationCurveBinding.Model)) { continue; } var gameObject = assetLoaderContext.GameObjects[animationCurveBinding.Model]; for (var j = 0; j < animationCurves.Count; j++) { var animationCurve = animationCurves[j]; var unityAnimationCurve = animationCurve.AnimationCurve; var gameObjectPath = assetLoaderContext.GameObjectPaths[gameObject]; var propertyName = animationCurve.Property; var propertyType = animationCurve.AnimatedType; // todo: working on it for a future release // the simplification isn't working with rotation curves yet //if (assetLoaderContext.Options.SimplifyAnimations) //{ // switch (propertyName) // { // case Constants.LocalRotationXProperty: // case Constants.LocalRotationYProperty: // case Constants.LocalRotationZProperty: // case Constants.LocalRotationWProperty: // unityAnimationCurve.Simplify(assetLoaderContext.Options.RotationThreshold, true); // break; // case Constants.LocalScaleXProperty: // case Constants.LocalScaleYProperty: // case Constants.LocalScaleZProperty: // unityAnimationCurve.Simplify(assetLoaderContext.Options.ScaleThreshold, true); // break; // default: // unityAnimationCurve.Simplify(assetLoaderContext.Options.PositionThreshold, true); // break; // } //} animationClip.SetCurve(gameObjectPath, propertyType, propertyName, unityAnimationCurve); } } //Fixed in Unity 2022.1.X if (assetLoaderContext.Options.EnsureQuaternionContinuity) { animationClip.EnsureQuaternionContinuity(); } return animationClip; } /// Converts the given Geometry Group into a Mesh. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. /// The Game Object where the Mesh belongs. /// The root Model. /// The Model used to generate the Game Object. private static void CreateGeometry(AssetLoaderContext assetLoaderContext, GameObject meshGameObject, IRootModel rootModel, IModel meshModel) { var geometryGroup = meshModel.GeometryGroup; if (geometryGroup.GeometriesData != null) { if (geometryGroup.Mesh == null) { geometryGroup.GenerateMesh(assetLoaderContext, assetLoaderContext.Options.AnimationType == AnimationType.None ? null : meshModel.BindPoses, meshModel.MaterialIndices); } assetLoaderContext.Allocations.Add(geometryGroup.Mesh); if (assetLoaderContext.Options.MarkMeshesAsDynamic) { geometryGroup.Mesh.MarkDynamic(); } if (assetLoaderContext.Options.LipSyncMappers != null) { Array.Sort(assetLoaderContext.Options.LipSyncMappers, (a, b) => a.CheckingOrder > b.CheckingOrder ? -1 : 1); foreach (var lipSyncMapper in assetLoaderContext.Options.LipSyncMappers) { if (lipSyncMapper.Map(assetLoaderContext, geometryGroup, out var visemeToBlendTargets)) { var lipSyncMapping = meshGameObject.AddComponent(); lipSyncMapping.VisemeToBlendTargets = visemeToBlendTargets; break; } } } if (assetLoaderContext.Options.GenerateColliders) { if (assetLoaderContext.RootModel.AllAnimations != null && assetLoaderContext.RootModel.AllAnimations.Count > 0 && assetLoaderContext.Options.ShowLoadingWarnings) { Debug.LogWarning("Adding a MeshCollider to an animated object."); } var meshCollider = meshGameObject.AddComponent(); meshCollider.sharedMesh = geometryGroup.Mesh; meshCollider.convex = assetLoaderContext.Options.ConvexColliders; } Renderer renderer = null; if (assetLoaderContext.Options.AnimationType != AnimationType.None || assetLoaderContext.Options.ImportBlendShapes) { var bones = assetLoaderContext.Options.AddAllBonesToSkinnedMeshRenderers ? GetAllBonesRecursive(assetLoaderContext) : meshModel.Bones; var geometryGroupBlendShapeGeometryBindings = geometryGroup.BlendShapeKeys; if ((bones != null && bones.Count > 0 || geometryGroupBlendShapeGeometryBindings != null && geometryGroupBlendShapeGeometryBindings.Count > 0) && assetLoaderContext.Options.AnimationType != AnimationType.None) { var skinnedMeshRenderer = meshGameObject.AddComponent(); skinnedMeshRenderer.sharedMesh = geometryGroup.Mesh; skinnedMeshRenderer.enabled = !assetLoaderContext.Options.ImportVisibility || meshModel.Visibility; if (bones != null && bones.Count > 0) { skinnedMeshRenderer.bones = new Transform[bones.Count]; } renderer = skinnedMeshRenderer; } } if (renderer == null) { var meshFilter = meshGameObject.AddComponent(); meshFilter.sharedMesh = geometryGroup.Mesh; if (!assetLoaderContext.Options.LoadPointClouds) { var meshRenderer = meshGameObject.AddComponent(); meshRenderer.enabled = !assetLoaderContext.Options.ImportVisibility || meshModel.Visibility; renderer = meshRenderer; } } if (renderer != null) { Material loadingMaterial = null; if (assetLoaderContext.Options.MaterialMappers != null) { for (var i = 0; i < assetLoaderContext.Options.MaterialMappers.Length; i++) { var mapper = assetLoaderContext.Options.MaterialMappers[i]; if (mapper != null && mapper.IsCompatible(null)) { loadingMaterial = mapper.LoadingMaterial; break; } } } var unityMaterials = new Material[geometryGroup.GeometriesData.Count]; if (loadingMaterial == null) { if (assetLoaderContext.Options.ShowLoadingWarnings) { Debug.LogWarning("Could not find a suitable loading Material."); } } else { for (var i = 0; i < unityMaterials.Length; i++) { unityMaterials[i] = loadingMaterial; } } renderer.sharedMaterials = unityMaterials; var materialIndices = meshModel.MaterialIndices; foreach (var geometryData in geometryGroup.GeometriesData) { var geometry = geometryData.Value; if (geometry == null) { continue; } var originalGeometryIndex = geometry.OriginalIndex; var materialIndex = materialIndices[originalGeometryIndex]; if (materialIndex < 0 || materialIndex >= rootModel.AllMaterials.Count) { continue; } var sourceMaterial = rootModel.AllMaterials[materialIndex]; if (sourceMaterial == null) { continue; } if (originalGeometryIndex < 0 || originalGeometryIndex >= renderer.sharedMaterials.Length) { continue; } var materialRenderersContext = new MaterialRendererContext { Context = assetLoaderContext, Renderer = renderer, GeometryIndex = geometry.Index, Material = sourceMaterial }; if (assetLoaderContext.MaterialRenderers.TryGetValue(sourceMaterial, out var materialRendererContextList)) { materialRendererContextList.Add(materialRenderersContext); } else { assetLoaderContext.MaterialRenderers.Add(sourceMaterial, new List { materialRenderersContext }); } } } } } /// /// Creates a list with every bone in the loaded model. /// private static IList GetAllBonesRecursive(AssetLoaderContext assetLoaderContext) { var bones = new List(); foreach (var model in assetLoaderContext.RootModel.AllModels) { if (model.IsBone) { bones.Add(model); } } return bones; } /// Loads the root Model. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void LoadModel(AssetLoaderContext assetLoaderContext) { SetupModelLoading(assetLoaderContext); } private static void SetupModelLoading(AssetLoaderContext assetLoaderContext) { if (assetLoaderContext.Stream == null && string.IsNullOrWhiteSpace(assetLoaderContext.Filename)) { throw new Exception("TriLib is unable to load the given file."); } if (assetLoaderContext.Options.MaterialMappers != null) { Array.Sort(assetLoaderContext.Options.MaterialMappers, (a, b) => a.CheckingOrder > b.CheckingOrder ? -1 : 1); } else { if (assetLoaderContext.Options.ShowLoadingWarnings) { Debug.LogWarning("Your AssetLoaderOptions instance has no MaterialMappers. TriLib can't process materials without them."); } } #if TRILIB_DRACO GltfReader.DracoDecompressorCallback = DracoMeshLoader.DracoDecompressorCallback; #endif var fileExtension = assetLoaderContext.FileExtension; if (fileExtension == null) { fileExtension = FileUtils.GetFileExtension(assetLoaderContext.Filename, false); } else if (fileExtension[0] == '.' && fileExtension.Length > 1) { fileExtension = fileExtension.Substring(1); } if (assetLoaderContext.Stream == null) { var fileStream = new FileStream(assetLoaderContext.Filename, FileMode.Open, FileAccess.Read, FileShare.Read); assetLoaderContext.Stream = fileStream; var reader = Readers.FindReaderForExtension(fileExtension); if (reader != null) { assetLoaderContext.RootModel = reader.ReadStream(fileStream, assetLoaderContext, assetLoaderContext.Filename, assetLoaderContext.OnProgress); } } else { var reader = Readers.FindReaderForExtension(fileExtension); if (reader != null) { assetLoaderContext.RootModel = reader.ReadStream(assetLoaderContext.Stream, assetLoaderContext, assetLoaderContext.Filename, assetLoaderContext.OnProgress); } else { throw new Exception("Could not find a suitable reader for the given model. Please fill the 'fileExtension' parameter when calling any model loading method."); } if (assetLoaderContext.RootModel == null) { throw new Exception("TriLib could not load the given model."); } } } /// Processes the root Model. /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void ProcessRootModel(AssetLoaderContext assetLoaderContext) { ProcessModel(assetLoaderContext); ProcessMaterials(assetLoaderContext); } /// /// Processes the Model Materials, if all source Materials have been loaded. /// /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void ProcessMaterials(AssetLoaderContext assetLoaderContext) { if (assetLoaderContext.RootModel?.AllMaterials != null && assetLoaderContext.RootModel.AllMaterials.Count > 0) { if (assetLoaderContext.Options.MaterialMappers != null) { ProcessMaterialRenderers(assetLoaderContext); } else if (assetLoaderContext.Options.ShowLoadingWarnings) { Debug.LogWarning("Please specify a TriLib Material Mapper, otherwise Materials can't be created."); } } else { if (assetLoaderContext.Options.DiscardUnusedTextures) { FinishLoading(assetLoaderContext); } else { LoadUnusedTextures(assetLoaderContext); } } } /// /// Loads the unused Textures and adds them to the allocations list. /// /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void LoadUnusedTextures(AssetLoaderContext assetLoaderContext) { if (assetLoaderContext.RootGameObject != null) { if (assetLoaderContext.RootModel.AllTextures != null) { foreach (var texture in assetLoaderContext.RootModel.AllTextures) { var textureLoadingContext = new TextureLoadingContext() { Texture = texture, Context = assetLoaderContext }; if (!assetLoaderContext.TryGetLoadedTexture(textureLoadingContext, out _)) { if (assetLoaderContext.Options.UseUnityNativeTextureLoader) { TextureLoaders.LoadTexture(textureLoadingContext); } else { TextureLoaders.CreateTexture(textureLoadingContext); TextureLoaders.LoadTexture(textureLoadingContext); } } } } } FinishLoading(assetLoaderContext); } /// /// Finishes the Model loading, calling the OnMaterialsLoad callback, if present. /// /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void FinishLoading(AssetLoaderContext assetLoaderContext) { if (assetLoaderContext.Options.AddAssetUnloader && (assetLoaderContext.RootGameObject != null || assetLoaderContext.WrapperGameObject != null)) { var gameObject = assetLoaderContext.RootGameObject ?? assetLoaderContext.WrapperGameObject; var assetUnloader = gameObject.AddComponent(); assetUnloader.Id = AssetUnloader.GetNextId(); assetUnloader.Allocations = assetLoaderContext.Allocations; assetUnloader.CustomData = assetLoaderContext.CustomData; } if (assetLoaderContext.Options.DiscardUnusedTextures) { assetLoaderContext.DiscardUnusedTextures(); } assetLoaderContext.Reader?.UpdateLoadingPercentage(1f, assetLoaderContext.Reader.LoadingStepsCount + (int)ReaderBase.PostLoadingSteps.FinishedProcessing); assetLoaderContext.OnMaterialsLoad?.Invoke(assetLoaderContext); Cleanup(assetLoaderContext); } /// /// Processes Model Renderers. /// /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void ProcessMaterialRenderers(AssetLoaderContext assetLoaderContext) { var materialMapperContexts = new MaterialMapperContext[assetLoaderContext.RootModel.AllMaterials.Count]; for (var i = 0; i < assetLoaderContext.RootModel.AllMaterials.Count; i++) { var material = assetLoaderContext.RootModel.AllMaterials[i]; var materialMapperContext = new MaterialMapperContext() { Context = assetLoaderContext, Material = material }; materialMapperContexts[i] = materialMapperContext; for (var j = 0; j < assetLoaderContext.Options.MaterialMappers.Length; j++) { var materialMapper = assetLoaderContext.Options.MaterialMappers[j]; if (materialMapper is object && materialMapper.IsCompatible(materialMapperContext)) { materialMapperContext.MaterialMapper = materialMapper; materialMapper.Map(materialMapperContext); ApplyMaterialToRenderers(materialMapperContext); //materialMapperContext.AddPostProcessingActionToMainThread(ApplyMaterialToRenderers, materialMapperContext); break; } } assetLoaderContext.Reader.UpdateLoadingPercentage(i, assetLoaderContext.Reader.LoadingStepsCount + (int)ReaderBase.PostLoadingSteps.PostProcessRenderers, assetLoaderContext.RootModel.AllMaterials.Count); } if (assetLoaderContext.Options.DiscardUnusedTextures) { FinishLoading(assetLoaderContext); //assetLoaderContext.ExecuteActionsQueue(FinishLoading); } else { LoadUnusedTextures(assetLoaderContext); //assetLoaderContext.ExecuteActionsQueue(LoadUnusedTextures); } } /// /// Applies the Material from the given context to its Renderers. /// /// The source Material Mapper Context, containing the Virtual Material and Unity Material. private static void ApplyMaterialToRenderers(MaterialMapperContext materialMapperContext) { materialMapperContext.Completed = false; if (materialMapperContext.Context.MaterialRenderers.TryGetValue(materialMapperContext.Material, out var materialRendererList)) { for (var k = 0; k < materialRendererList.Count; k++) { var materialRendererContext = materialRendererList[k]; materialRendererContext.MaterialMapperContext = materialMapperContext; materialMapperContext.MaterialMapper.ApplyMaterialToRenderer(materialRendererContext); } } materialMapperContext.Completed = true; } /// Handles all Model loading errors, unloads the partially loaded Model (if suitable), and calls the error callback (if existing). /// The Contextualized Error that has occurred. private static void HandleError(IContextualizedError error) { var exception = error.GetInnerException(); if (error.GetContext() is IAssetLoaderContext context) { var assetLoaderContext = context.Context; if (assetLoaderContext != null) { Cleanup(assetLoaderContext); if (assetLoaderContext.Options.DestroyOnError && assetLoaderContext.RootGameObject != null) { if (!Application.isPlaying) { Object.DestroyImmediate(assetLoaderContext.RootGameObject); } else { Object.Destroy(assetLoaderContext.RootGameObject); } assetLoaderContext.RootGameObject = null; } if (assetLoaderContext.OnError != null) { Dispatcher.InvokeAsync(assetLoaderContext.OnError, error); } } } else { var contextualizedError = new ContextualizedError(exception, null); Dispatcher.InvokeAsync(Rethrow, contextualizedError); } } /// /// Tries to close the Model Stream, if the used AssetLoaderOptions.CloseSteamAutomatically option is enabled. /// /// The Asset Loader Context reference. Asset Loader Context contains the Model loading data. private static void Cleanup(AssetLoaderContext assetLoaderContext) { if (assetLoaderContext.Stream != null && assetLoaderContext.Options.CloseStreamAutomatically) { assetLoaderContext.Stream.TryToDispose(); } //Resources.UnloadUnusedAssets(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } /// Throws the given Contextualized Error on the main Thread. /// /// The Contextualized Error to throw. private static void Rethrow(ContextualizedError contextualizedError) { throw contextualizedError; } } }