// SPDX-FileCopyrightText: 2023 Unity Technologies and the glTFast authors
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
namespace GLTFast.Export
{
using Logging;
///
/// Creates glTF files from GameObject hierarchies
///
public class GameObjectExport
{
GltfWriter m_Writer;
IMaterialExport m_MaterialExport;
GameObjectExportSettings m_Settings;
///
/// Provides glTF export of GameObject based scenes and hierarchies.
///
/// Export settings
/// GameObject export settings
/// Provides material conversion
/// Defer agent (<see cref="IDeferAgent"/>); decides when/if to preempt
/// export to preserve a stable frame rate.
/// Interface for logging (error) messages.
public GameObjectExport(
ExportSettings exportSettings = null,
GameObjectExportSettings gameObjectExportSettings = null,
IMaterialExport materialExport = null,
IDeferAgent deferAgent = null,
ICodeLogger logger = null
)
{
m_Settings = gameObjectExportSettings ?? new GameObjectExportSettings();
m_Writer = new GltfWriter(exportSettings, deferAgent, logger);
m_MaterialExport = materialExport ?? MaterialExport.GetDefaultMaterialExport();
}
///
/// Adds a scene to the glTF which consists of a collection of GameObjects.
///
/// GameObjects to be added (recursively) as root level nodes.
/// Name of the scene
/// True, if the scene was added flawlessly. False, otherwise
public bool AddScene(GameObject[] gameObjects, string name = null)
{
return AddScene(gameObjects, float4x4.identity, name);
}
///
/// Creates a glTF scene from a collection of GameObjects. The GameObjects will be converted into glTF nodes.
/// The nodes' positions within the glTF scene will be their GameObjects' world position transformed by the
/// matrix, essentially allowing you to set an arbitrary scene center.
///
/// Root level GameObjects (will get added recursively)
/// Inverse scene origin matrix. This transform will be applied to all nodes.
/// Name of the scene
/// True if the scene was added successfully, false otherwise
public bool AddScene(ICollection gameObjects, float4x4 origin, string name)
{
CertifyNotDisposed();
var rootNodes = new List(gameObjects.Count);
var tempMaterials = new List();
var success = true;
var nodesQueue = new Queue();
var transformNodeId = new Dictionary();
foreach (var gameObject in gameObjects)
{
success &= AddGameObject(
gameObject,
origin,
nodesQueue,
transformNodeId,
out var nodeId
);
if (nodeId >= 0)
{
rootNodes.Add((uint)nodeId);
}
}
while (nodesQueue.Count > 0)
{
var transform = nodesQueue.Dequeue();
AddNodeComponents(
transform,
transformNodeId,
tempMaterials
);
}
if (rootNodes.Count > 0)
{
m_Writer.AddScene(rootNodes.ToArray(), name);
}
return success;
}
///
/// Exports the collected scenes/content as glTF, writes it to a file
/// and disposes this object.
/// After the export this instance cannot be re-used!
///
/// glTF destination file path
/// Token to submit cancellation requests. The default value is None.
/// True if the glTF file was created successfully, false otherwise
public async Task SaveToFileAndDispose(
string path,
CancellationToken cancellationToken = default
)
{
CertifyNotDisposed();
var success = await m_Writer.SaveToFileAndDispose(path);
m_Writer = null;
return success;
}
///
/// Exports the collected scenes/content as glTF, writes it to a Stream
/// and disposes this object. Only works for self-contained glTF-Binary.
/// After the export this instance cannot be re-used!
///
/// glTF destination stream
/// Token to submit cancellation requests. The default value is None.
/// True if the glTF file was written successfully, false otherwise
public async Task SaveToStreamAndDispose(
Stream stream,
CancellationToken cancellationToken = default
)
{
CertifyNotDisposed();
var success = await m_Writer.SaveToStreamAndDispose(stream);
m_Writer = null;
return success;
}
void CertifyNotDisposed()
{
if (m_Writer == null)
{
throw new InvalidOperationException("GameObjectExport was already disposed");
}
}
bool AddGameObject(
GameObject gameObject,
float4x4? sceneOrigin,
Queue nodesQueue,
Dictionary transformNodeId,
out int nodeId)
{
if (m_Settings.OnlyActiveInHierarchy && !gameObject.activeInHierarchy
|| gameObject.CompareTag("EditorOnly"))
{
nodeId = -1;
return true;
}
var success = true;
var childCount = gameObject.transform.childCount;
uint[] children = null;
if (childCount > 0)
{
var childList = new List(gameObject.transform.childCount);
for (var i = 0; i < childCount; i++)
{
var child = gameObject.transform.GetChild(i);
success &= AddGameObject(
child.gameObject,
null,
nodesQueue,
transformNodeId,
out var childNodeId
);
if (childNodeId >= 0)
{
childList.Add((uint)childNodeId);
}
}
if (childList.Count > 0)
{
children = childList.ToArray();
}
}
var transform = gameObject.transform;
var onIncludedLayer = ((1 << gameObject.layer) & m_Settings.LayerMask) != 0;
if (onIncludedLayer || children != null)
{
float3 translation;
quaternion rotation;
float3 scale;
if (sceneOrigin.HasValue)
{
// root level node - calculate transform based on scene origin
var trans = math.mul(sceneOrigin.Value, transform.localToWorldMatrix);
trans.Decompose(out translation, out rotation, out scale);
}
else
{
// nested node - use local transform
translation = transform.localPosition;
rotation = transform.localRotation;
scale = transform.localScale;
}
var newNodeId = m_Writer.AddNode(
translation,
rotation,
scale,
children,
gameObject.name
);
if (onIncludedLayer)
{
nodesQueue.Enqueue(transform);
}
transformNodeId[transform] = newNodeId;
nodeId = (int)newNodeId;
}
else
{
nodeId = -1;
}
return success;
}
void AddNodeComponents(
Transform transform,
Dictionary transformNodeId,
List tempMaterials
)
{
var gameObject = transform.gameObject;
var nodeId = transformNodeId[transform];
tempMaterials.Clear();
Mesh mesh = null;
Transform[] bones = null;
if (gameObject.TryGetComponent(out MeshFilter meshFilter))
{
if (gameObject.TryGetComponent(out Renderer renderer))
{
if (renderer.enabled || m_Settings.DisabledComponents)
{
mesh = meshFilter.sharedMesh;
renderer.GetSharedMaterials(tempMaterials);
}
}
}
else
if (gameObject.TryGetComponent(out SkinnedMeshRenderer smr))
{
if (smr.enabled || m_Settings.DisabledComponents)
{
mesh = smr.sharedMesh;
bones = smr.bones;
smr.GetSharedMaterials(tempMaterials);
}
}
var materialIds = new int[tempMaterials.Count];
for (var i = 0; i < tempMaterials.Count; i++)
{
var uMaterial = tempMaterials[i];
if (uMaterial != null && m_Writer.AddMaterial(uMaterial, out var materialId, m_MaterialExport))
{
materialIds[i] = materialId;
}
else
{
materialIds[i] = -1;
}
}
if (mesh != null)
{
uint[] joints = null;
if (bones != null)
{
joints = new uint[bones.Length];
for (var i = 0; i < bones.Length; i++)
{
var bone = bones[i];
if (!transformNodeId.TryGetValue(bone, out joints[i]))
{
#if DEBUG
Debug.LogError($"Skip skin on {transform.name}: No node ID for bone transform {bone.name} found!");
joints = null;
break;
#endif
}
}
}
m_Writer.AddMeshToNode((int)nodeId, mesh, materialIds, joints);
}
if (gameObject.TryGetComponent(out Camera camera))
{
if (camera.enabled || m_Settings.DisabledComponents)
{
if (m_Writer.AddCamera(camera, out var cameraId))
{
m_Writer.AddCameraToNode((int)nodeId, cameraId);
}
}
}
if (gameObject.TryGetComponent(out Light light))
{
if (light.enabled || m_Settings.DisabledComponents)
{
if (m_Writer.AddLight(light, out var lightId))
{
m_Writer.AddLightToNode((int)nodeId, lightId);
}
}
}
}
}
}