// SPDX-FileCopyrightText: 2023 Unity Technologies and the glTFast authors // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using GLTFast.Schema; using Unity.Collections; using UnityEngine; using UnityEngine.Profiling; #if UNITY_ANIMATION using Animation = UnityEngine.Animation; #endif using Camera = UnityEngine.Camera; using Material = UnityEngine.Material; using Mesh = UnityEngine.Mesh; // #if UNITY_EDITOR && UNITY_ANIMATION // using UnityEditor.Animations; // #endif namespace GLTFast { using Logging; /// /// Generates a GameObject hierarchy from a glTF scene /// public class GameObjectInstantiator : IInstantiator { // Developers might want to customize this class by deriving from it. // Hence some members need to stay protected (not private) // ReSharper disable MemberCanBePrivate.Global /// /// Instantiation settings /// protected InstantiationSettings m_Settings; /// /// Instantiation logger /// protected ICodeLogger m_Logger; /// /// glTF to instantiate from /// protected IGltfReadable m_Gltf; /// /// Generated GameObjects will get parented to this Transform /// protected Transform m_Parent; /// /// glTF node index to instantiated GameObject dictionary /// protected Dictionary m_Nodes; List m_InstanceSlots; /// /// Transform representing the scene. /// Root nodes will get parented to it. /// public Transform SceneTransform { get; protected set; } /// /// Contains information about the latest instance of a glTF scene /// public GameObjectSceneInstance SceneInstance { get; protected set; } // ReSharper restore MemberCanBePrivate.Global /// /// Constructs a GameObjectInstantiator /// /// glTF to instantiate from /// Generated GameObjects will get parented to this Transform /// Custom logger /// Instantiation settings public GameObjectInstantiator( IGltfReadable gltf, Transform parent, ICodeLogger logger = null, InstantiationSettings settings = null ) { this.m_Gltf = gltf; this.m_Parent = parent; m_Logger = logger; m_Settings = settings ?? new InstantiationSettings(); } /// public virtual void BeginScene( string name, uint[] rootNodeIndices ) { Profiler.BeginSample("BeginScene"); m_Nodes = new Dictionary(); SceneInstance = new GameObjectSceneInstance(); GameObject sceneGameObject; if (m_Settings.SceneObjectCreation == SceneObjectCreation.Never || m_Settings.SceneObjectCreation == SceneObjectCreation.WhenMultipleRootNodes && rootNodeIndices.Length == 1) { sceneGameObject = m_Parent.gameObject; } else { sceneGameObject = new GameObject(name ?? "Scene"); sceneGameObject.transform.SetParent(m_Parent, false); sceneGameObject.layer = m_Settings.Layer; } SceneTransform = sceneGameObject.transform; Profiler.EndSample(); } #if UNITY_ANIMATION /// public virtual void AddAnimation(AnimationClip[] animationClips) { if ((m_Settings.Mask & ComponentType.Animation) != 0 && animationClips != null) { // we want to create an Animator for non-legacy clips, and an Animation component for legacy clips. var isLegacyAnimation = animationClips.Length > 0 && animationClips[0].legacy; // #if UNITY_EDITOR // // This variant creates a Mecanim Animator and AnimationController // // which does not work at runtime. It's kept for potential Editor import usage // if(!isLegacyAnimation) { // var animator = go.AddComponent(); // var controller = new UnityEditor.Animations.AnimatorController(); // controller.name = animator.name; // controller.AddLayer("Default"); // controller.layers[0].defaultWeight = 1; // for (var index = 0; index < animationClips.Length; index++) { // var clip = animationClips[index]; // // controller.AddLayer(clip.name); // // controller.layers[index].defaultWeight = 1; // var state = controller.AddMotion(clip, 0); // controller.AddParameter("Test", AnimatorControllerParameterType.Bool); // // var stateMachine = controller.layers[0].stateMachine; // // UnityEditor.Animations.AnimatorState entryState = null; // // var state = stateMachine.AddState(clip.name); // // state.motion = clip; // // var loopTransition = state.AddTransition(state); // // loopTransition.hasExitTime = true; // // loopTransition.duration = 0; // // loopTransition.exitTime = 0; // // entryState = state; // // stateMachine.AddEntryTransition(entryState); // // UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath // } // // animator.runtimeAnimatorController = controller; // // // for (var index = 0; index < animationClips.Length; index++) { // // controller.layers[index].blendingMode = UnityEditor.Animations.AnimatorLayerBlendingMode.Additive; // // animator.SetLayerWeight(index,1); // // } // } // #endif // UNITY_EDITOR if(isLegacyAnimation) { var animation = SceneTransform.gameObject.AddComponent(); for (var index = 0; index < animationClips.Length; index++) { var clip = animationClips[index]; animation.AddClip(clip,clip.name); if (index < 1) { animation.clip = clip; } } SceneInstance.SetLegacyAnimation(animation); } else { SceneTransform.gameObject.AddComponent(); } } } #endif // UNITY_ANIMATION /// public void CreateNode( uint nodeIndex, uint? parentIndex, Vector3 position, Quaternion rotation, Vector3 scale ) { var go = new GameObject(); // Deactivate root-level nodes, so half-loaded scenes won't render. go.SetActive(parentIndex.HasValue); go.transform.localScale = scale; go.transform.localPosition = position; go.transform.localRotation = rotation; go.layer = m_Settings.Layer; m_Nodes[nodeIndex] = go; go.transform.SetParent( parentIndex.HasValue ? m_Nodes[parentIndex.Value].transform : SceneTransform, false); NodeCreated?.Invoke(nodeIndex, go); } /// public virtual void SetNodeName(uint nodeIndex, string name) { m_Nodes[nodeIndex].name = name ?? $"Node-{nodeIndex}"; } /// public virtual void AddPrimitive( uint nodeIndex, string meshName, MeshResult meshResult, uint[] joints = null, uint? rootJoint = null, float[] morphTargetWeights = null, int meshNumeration = 0 ) { if ((m_Settings.Mask & ComponentType.Mesh) == 0) { return; } GameObject meshGo; if (meshNumeration == 0) { // Use Node GameObject for first Primitive meshGo = m_Nodes[nodeIndex]; } else { meshGo = new GameObject(meshName); meshGo.transform.SetParent(m_Nodes[nodeIndex].transform, false); meshGo.layer = m_Settings.Layer; } Renderer renderer; var hasMorphTargets = meshResult.mesh.blendShapeCount > 0; if (joints == null && !hasMorphTargets) { var mf = meshGo.AddComponent(); mf.mesh = meshResult.mesh; var mr = meshGo.AddComponent(); renderer = mr; } else { var smr = meshGo.AddComponent(); smr.updateWhenOffscreen = m_Settings.SkinUpdateWhenOffscreen; if (joints != null) { var bones = new Transform[joints.Length]; for (var j = 0; j < bones.Length; j++) { var jointIndex = joints[j]; bones[j] = m_Nodes[jointIndex].transform; } smr.bones = bones; if (rootJoint.HasValue) { smr.rootBone = m_Nodes[rootJoint.Value].transform; } } smr.sharedMesh = meshResult.mesh; if (morphTargetWeights != null) { for (var i = 0; i < morphTargetWeights.Length; i++) { var weight = morphTargetWeights[i]; smr.SetBlendShapeWeight(i, weight); } } renderer = smr; } var materials = new Material[meshResult.materialIndices.Length]; for (var index = 0; index < materials.Length; index++) { var material = m_Gltf.GetMaterial(meshResult.materialIndices[index]) ?? m_Gltf.GetDefaultMaterial(); materials[index] = material; } renderer.sharedMaterials = materials; var slots = m_Gltf.GetMaterialsVariantsSlots(meshResult.meshIndex, meshNumeration); if (slots != null && slots.Length > 0) { m_InstanceSlots ??= new List(); var instanceSlot = new MaterialsVariantsSlotInstances(renderer, slots); m_InstanceSlots.Add(instanceSlot); } MeshAdded?.Invoke( meshGo, nodeIndex, meshName, meshResult, joints, rootJoint, morphTargetWeights, meshNumeration ); } /// public virtual void AddPrimitiveInstanced( uint nodeIndex, string meshName, MeshResult meshResult, uint instanceCount, NativeArray? positions, NativeArray? rotations, NativeArray? scales, int meshNumeration = 0 ) { if ((m_Settings.Mask & ComponentType.Mesh) == 0) { return; } var materials = new Material[meshResult.materialIndices.Length]; for (var index = 0; index < materials.Length; index++) { var material = m_Gltf.GetMaterial(meshResult.materialIndices[index]) ?? m_Gltf.GetDefaultMaterial(); material.enableInstancing = true; materials[index] = material; } var slots = m_Gltf.GetMaterialsVariantsSlots(meshResult.meshIndex, meshNumeration); var hasMaterialsVariants = slots != null && slots.Length > 0; var renderers = hasMaterialsVariants ? new Renderer[instanceCount] : null; for (var i = 0; i < instanceCount; i++) { var meshGo = new GameObject($"{meshName}_i{i}"); meshGo.layer = m_Settings.Layer; var t = meshGo.transform; t.SetParent(m_Nodes[nodeIndex].transform, false); t.localPosition = positions?[i] ?? Vector3.zero; t.localRotation = rotations?[i] ?? Quaternion.identity; t.localScale = scales?[i] ?? Vector3.one; var mf = meshGo.AddComponent(); mf.mesh = meshResult.mesh; Renderer renderer = meshGo.AddComponent(); renderer.sharedMaterials = materials; if (hasMaterialsVariants) { renderers[i] = renderer; } } if (hasMaterialsVariants) { m_InstanceSlots ??= new List(); var instanceSlot = new MultiMaterialsVariantsSlotInstances(renderers, slots); m_InstanceSlots.Add(instanceSlot); } } /// public virtual void AddCamera(uint nodeIndex, uint cameraIndex) { if ((m_Settings.Mask & ComponentType.Camera) == 0) { return; } var camera = m_Gltf.GetSourceCamera(cameraIndex); switch (camera.GetCameraType()) { case Schema.Camera.Type.Orthographic: var o = camera.Orthographic; AddCameraOrthographic( nodeIndex, o.znear, o.zfar >= 0 ? o.zfar : (float?)null, o.xmag, o.ymag, camera.name ); break; case Schema.Camera.Type.Perspective: var p = camera.Perspective; AddCameraPerspective( nodeIndex, p.yfov, p.znear, p.zfar, p.aspectRatio > 0 ? p.aspectRatio : (float?)null, camera.name ); break; } } void AddCameraPerspective( uint nodeIndex, float verticalFieldOfView, float nearClipPlane, float farClipPlane, // ReSharper disable once UnusedParameter.Local float? aspectRatio, string cameraName ) { var cam = CreateCamera(nodeIndex, cameraName, out var localScale); cam.orthographic = false; cam.fieldOfView = verticalFieldOfView * Mathf.Rad2Deg; cam.nearClipPlane = nearClipPlane * localScale; cam.farClipPlane = farClipPlane * localScale; // // If the aspect ratio is given and does not match the // // screen's aspect ratio, the viewport rect is reduced // // to match the glTFs aspect ratio (box fit) // if (aspectRatio.HasValue) { // cam.rect = GetLimitedViewPort(aspectRatio.Value); // } } void AddCameraOrthographic( uint nodeIndex, float nearClipPlane, float? farClipPlane, float horizontal, float vertical, string cameraName ) { var cam = CreateCamera(nodeIndex, cameraName, out var localScale); var farValue = farClipPlane ?? float.MaxValue; cam.orthographic = true; cam.nearClipPlane = nearClipPlane * localScale; cam.farClipPlane = farValue * localScale; cam.orthographicSize = vertical; // Note: Ignores `horizontal` // Custom projection matrix // Ignores screen's aspect ratio cam.projectionMatrix = Matrix4x4.Ortho( -horizontal, horizontal, -vertical, vertical, nearClipPlane, farValue ); // // If the aspect ratio does not match the // // screen's aspect ratio, the viewport rect is reduced // // to match the glTFs aspect ratio (box fit) // var aspectRatio = horizontal / vertical; // cam.rect = GetLimitedViewPort(aspectRatio); } /// /// Creates a camera component on the given node and returns an approximated /// local-to-world scale factor, required to counteract that Unity scales /// near- and far-clipping-planes via Transform. /// /// Node's index /// Camera's name /// Approximated local-to-world scale factor /// The newly created Camera component Camera CreateCamera(uint nodeIndex, string cameraName, out float localScale) { var cameraParent = m_Nodes[nodeIndex]; var camGo = new GameObject( string.IsNullOrEmpty(cameraName) ? $"Camera-{nodeIndex}" : $"{cameraParent.name}-Camera" ); camGo.layer = m_Settings.Layer; var camTrans = camGo.transform; var parentTransform = cameraParent.transform; camTrans.SetParent(parentTransform, false); var tmp = Quaternion.Euler(0, 180, 0); camTrans.localRotation = tmp; var cam = camGo.AddComponent(); // By default, imported cameras are not enabled by default cam.enabled = false; SceneInstance.AddCamera(cam); var parentScale = parentTransform.localToWorldMatrix.lossyScale; localScale = (parentScale.x + parentScale.y + parentScale.y) / 3; return cam; } // static Rect GetLimitedViewPort(float aspectRatio) { // var screenAspect = Screen.width / (float)Screen.height; // if (Mathf.Abs(1 - (screenAspect / aspectRatio)) <= math.EPSILON) { // // Identical aspect ratios // return new Rect(0,0,1,1); // } // if (aspectRatio < screenAspect) { // var w = aspectRatio / screenAspect; // return new Rect((1 - w) / 2, 0, w, 1f); // } else { // var h = screenAspect / aspectRatio; // return new Rect(0, (1 - h) / 2, 1f, h); // } // } /// public void AddLightPunctual( uint nodeIndex, uint lightIndex ) { if ((m_Settings.Mask & ComponentType.Light) == 0) { return; } var lightGameObject = m_Nodes[nodeIndex]; var lightSource = m_Gltf.GetSourceLightPunctual(lightIndex); if (lightSource.GetLightType() != LightPunctual.Type.Point) { // glTF lights' direction is flipped, compared with Unity's, so // we're adding a rotated child GameObject to counteract. var tmp = new GameObject($"{lightGameObject.name}_Orientation"); tmp.transform.SetParent(lightGameObject.transform, false); tmp.transform.localEulerAngles = new Vector3(0, 180, 0); lightGameObject = tmp; } var light = lightGameObject.AddComponent(); lightSource.ToUnityLight(light, m_Settings.LightIntensityFactor); SceneInstance.AddLight(light); } /// public virtual void EndScene(uint[] rootNodeIndices) { Profiler.BeginSample("EndScene"); if (m_InstanceSlots != null) { var materialsVariantsControl = new MaterialsVariantsControl(m_Gltf, m_InstanceSlots); SceneInstance.SetMaterialsVariantsControl(materialsVariantsControl); } if (rootNodeIndices != null) { foreach (var nodeIndex in rootNodeIndices) { m_Nodes[nodeIndex].SetActive(true); } } EndSceneCompleted?.Invoke(); Profiler.EndSample(); } /// /// Information for when a node's GameObject has been created. /// /// Index of the corresponding glTF node. /// GameObject that was created. public delegate void NodeCreatedDelegate( uint nodeIndex, GameObject gameObject ); /// /// Provides information for when a mesh was added to a node GameObject /// /// GameObject that holds the Mesh. /// Index of the node /// Mesh's name /// The converted Mesh /// If a skin was attached, the joint indices. Null otherwise /// Root joint node index, if present /// Morph target weights, if present /// Per glTF mesh numeration. A glTF mesh is converted /// into one or more MeshResults which are numbered consecutively. public delegate void MeshAddedDelegate( GameObject gameObject, uint nodeIndex, string meshName, MeshResult meshResult, uint[] joints = null, uint? rootJoint = null, float[] morphTargetWeights = null, int meshNumeration = 0 ); /// Invoked when a node's GameObject has been created. public event NodeCreatedDelegate NodeCreated; /// Invoked after a mesh was added to a node GameObject public event MeshAddedDelegate MeshAdded; /// Invoked after a scene has been instantiated. public event Action EndSceneCompleted; } }