Files
AR-Menu/Library/PackageCache/com.unity.cloud.gltfast@db5a82ec0b47/Runtime/Scripts/GameObjectInstantiator.cs
2025-11-30 08:35:03 +02:00

614 lines
23 KiB
C#

// 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;
/// <summary>
/// Generates a GameObject hierarchy from a glTF scene
/// </summary>
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
/// <summary>
/// Instantiation settings
/// </summary>
protected InstantiationSettings m_Settings;
/// <summary>
/// Instantiation logger
/// </summary>
protected ICodeLogger m_Logger;
/// <summary>
/// glTF to instantiate from
/// </summary>
protected IGltfReadable m_Gltf;
/// <summary>
/// Generated GameObjects will get parented to this Transform
/// </summary>
protected Transform m_Parent;
/// <summary>
/// glTF node index to instantiated GameObject dictionary
/// </summary>
protected Dictionary<uint, GameObject> m_Nodes;
List<IMaterialsVariantsSlotInstance> m_InstanceSlots;
/// <summary>
/// Transform representing the scene.
/// Root nodes will get parented to it.
/// </summary>
public Transform SceneTransform { get; protected set; }
/// <summary>
/// Contains information about the latest instance of a glTF scene
/// </summary>
public GameObjectSceneInstance SceneInstance { get; protected set; }
// ReSharper restore MemberCanBePrivate.Global
/// <summary>
/// Constructs a GameObjectInstantiator
/// </summary>
/// <param name="gltf">glTF to instantiate from</param>
/// <param name="parent">Generated GameObjects will get parented to this Transform</param>
/// <param name="logger">Custom logger</param>
/// <param name="settings">Instantiation settings</param>
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();
}
/// <inheritdoc />
public virtual void BeginScene(
string name,
uint[] rootNodeIndices
)
{
Profiler.BeginSample("BeginScene");
m_Nodes = new Dictionary<uint, GameObject>();
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
/// <inheritdoc />
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<Animator>();
// 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<Animation>();
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<Animator>();
}
}
}
#endif // UNITY_ANIMATION
/// <inheritdoc />
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);
}
/// <inheritdoc />
public virtual void SetNodeName(uint nodeIndex, string name)
{
m_Nodes[nodeIndex].name = name ?? $"Node-{nodeIndex}";
}
/// <inheritdoc />
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<MeshFilter>();
mf.mesh = meshResult.mesh;
var mr = meshGo.AddComponent<MeshRenderer>();
renderer = mr;
}
else
{
var smr = meshGo.AddComponent<SkinnedMeshRenderer>();
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<IMaterialsVariantsSlotInstance>();
var instanceSlot = new MaterialsVariantsSlotInstances(renderer, slots);
m_InstanceSlots.Add(instanceSlot);
}
MeshAdded?.Invoke(
meshGo,
nodeIndex,
meshName,
meshResult,
joints,
rootJoint,
morphTargetWeights,
meshNumeration
);
}
/// <inheritdoc />
public virtual void AddPrimitiveInstanced(
uint nodeIndex,
string meshName,
MeshResult meshResult,
uint instanceCount,
NativeArray<Vector3>? positions,
NativeArray<Quaternion>? rotations,
NativeArray<Vector3>? 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<MeshFilter>();
mf.mesh = meshResult.mesh;
Renderer renderer = meshGo.AddComponent<MeshRenderer>();
renderer.sharedMaterials = materials;
if (hasMaterialsVariants)
{
renderers[i] = renderer;
}
}
if (hasMaterialsVariants)
{
m_InstanceSlots ??= new List<IMaterialsVariantsSlotInstance>();
var instanceSlot = new MultiMaterialsVariantsSlotInstances(renderers, slots);
m_InstanceSlots.Add(instanceSlot);
}
}
/// <inheritdoc />
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="nodeIndex">Node's index</param>
/// <param name="cameraName">Camera's name</param>
/// <param name="localScale">Approximated local-to-world scale factor</param>
/// <returns>The newly created Camera component</returns>
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<Camera>();
// 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);
// }
// }
/// <inheritdoc />
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<Light>();
lightSource.ToUnityLight(light, m_Settings.LightIntensityFactor);
SceneInstance.AddLight(light);
}
/// <inheritdoc />
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();
}
/// <summary>
/// Information for when a node's GameObject has been created.
/// </summary>
/// <param name="nodeIndex">Index of the corresponding glTF node.</param>
/// <param name="gameObject">GameObject that was created.</param>
public delegate void NodeCreatedDelegate(
uint nodeIndex,
GameObject gameObject
);
/// <summary>
/// Provides information for when a mesh was added to a node GameObject
/// </summary>
/// <param name="gameObject">GameObject that holds the Mesh.</param>
/// <param name="nodeIndex">Index of the node</param>
/// <param name="meshName">Mesh's name</param>
/// <param name="meshResult">The converted Mesh</param>
/// <param name="joints">If a skin was attached, the joint indices. Null otherwise</param>
/// <param name="rootJoint">Root joint node index, if present</param>
/// <param name="morphTargetWeights">Morph target weights, if present</param>
/// <param name="meshNumeration">Per glTF mesh <see cref="MeshResult"/> numeration. A glTF mesh is converted
/// into one or more MeshResults which are numbered consecutively.</param>
public delegate void MeshAddedDelegate(
GameObject gameObject,
uint nodeIndex,
string meshName,
MeshResult meshResult,
uint[] joints = null,
uint? rootJoint = null,
float[] morphTargetWeights = null,
int meshNumeration = 0
);
/// <summary>Invoked when a node's GameObject has been created.</summary>
public event NodeCreatedDelegate NodeCreated;
/// <summary>Invoked after a mesh was added to a node GameObject</summary>
public event MeshAddedDelegate MeshAdded;
/// <summary>Invoked after a scene has been instantiated.</summary>
public event Action EndSceneCompleted;
}
}