// 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;
}
}