#if UNITY_EDITOR #define ANIMATION_EXPORT_SUPPORTED #endif #if ANIMATION_EXPORT_SUPPORTED && (UNITY_ANIMATION || !UNITY_2019_1_OR_NEWER) #define ANIMATION_SUPPORTED #endif using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using GLTF.Schema; using Unity.Profiling; using UnityEngine; using UnityGLTF.Extensions; using UnityGLTF.Plugins; namespace UnityGLTF { public class ExportContext { public bool TreatEmptyRootAsScene = false; public bool MergeClipsWithMatchingNames = false; public LayerMask ExportLayers = -1; public ILogger logger; internal readonly GLTFSettings settings; public ExportContext() : this(GLTFSettings.GetOrCreateSettings()) { } public ExportContext(GLTFSettings settings) { if (!settings) settings = GLTFSettings.GetOrCreateSettings(); if (settings.UseMainCameraVisibility) ExportLayers = Camera.main ? Camera.main.cullingMask : -1; this.settings = settings; } public GLTFSceneExporter.RetrieveTexturePathDelegate TexturePathRetriever = (texture) => texture.name; // TODO Should we make all the callbacks on ExportContext obsolete? // Pro: We can remove them from the API // Con: No direct way to "just add callbacks" right now, always needs a plugin. // See GLTFSceneExporter for a case here we "just want callbacks" instead of a new class/context public GLTFSceneExporter.AfterSceneExportDelegate AfterSceneExport; public GLTFSceneExporter.BeforeSceneExportDelegate BeforeSceneExport; public GLTFSceneExporter.AfterNodeExportDelegate AfterNodeExport; public GLTFSceneExporter.BeforeMaterialExportDelegate BeforeMaterialExport; public GLTFSceneExporter.AfterMaterialExportDelegate AfterMaterialExport; public GLTFSceneExporter.BeforeTextureExportDelegate BeforeTextureExport; public GLTFSceneExporter.AfterTextureExportDelegate AfterTextureExport; public GLTFSceneExporter.AfterPrimitiveExportDelegate AfterPrimitiveExport; public GLTFSceneExporter.AfterMeshExportDelegate AfterMeshExport; internal GLTFExportPluginContext GetExportContextCallbacks() => new ExportContextCallbacks(this); #pragma warning disable CS0618 // Type or member is obsolete internal class ExportContextCallbacks : GLTFExportPluginContext { private readonly ExportContext _exportContext; internal ExportContextCallbacks(ExportContext context) { _exportContext = context; } public override void BeforeSceneExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot) => _exportContext.BeforeSceneExport?.Invoke(exporter, gltfRoot); public override void AfterSceneExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot) => _exportContext.AfterSceneExport?.Invoke(exporter, gltfRoot); public override void AfterNodeExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Transform transform, Node node) => _exportContext.AfterNodeExport?.Invoke(exporter, gltfRoot, transform, node); public override bool BeforeMaterialExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Material material, GLTFMaterial materialNode) { // static callback, run after options callback // we're iterating here because we want to stop calling any once we hit one that can export this material. if (_exportContext.BeforeMaterialExport != null) { var list = _exportContext.BeforeMaterialExport.GetInvocationList(); foreach (var entry in list) { var cb = (GLTFSceneExporter.BeforeMaterialExportDelegate) entry; if (cb != null && cb.Invoke(exporter, gltfRoot, material, materialNode)) { return true; } } } return false; } public override void AfterMaterialExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Material material, GLTFMaterial materialNode) => _exportContext.AfterMaterialExport?.Invoke(exporter, gltfRoot, material, materialNode); public override void BeforeTextureExport(GLTFSceneExporter exporter, ref GLTFSceneExporter.UniqueTexture texture, string textureSlot) => _exportContext.BeforeTextureExport?.Invoke(exporter, ref texture, textureSlot); public override void AfterTextureExport(GLTFSceneExporter exporter, GLTFSceneExporter.UniqueTexture texture, int index, GLTFTexture tex) => _exportContext.AfterTextureExport?.Invoke(exporter, texture, index, tex); public override void AfterPrimitiveExport(GLTFSceneExporter exporter, Mesh mesh, MeshPrimitive primitive, int index) => _exportContext.AfterPrimitiveExport?.Invoke(exporter, mesh, primitive, index); public override void AfterMeshExport(GLTFSceneExporter exporter, Mesh mesh, GLTFMesh gltfMesh, int index) => _exportContext.AfterMeshExport?.Invoke(exporter, mesh, gltfMesh, index); } #pragma warning restore CS0618 // Type or member is obsolete } [Obsolete("Use UnityGLTF.ExportContext instead. (UnityUpgradable) -> UnityGLTF.ExportContext")] public class ExportOptions: ExportContext { public ExportOptions(): base() { } public ExportOptions(GLTFSettings settings): base(settings) { } } public partial class GLTFSceneExporter { // Available export callbacks. // Callbacks can be either set statically (for exporters that register themselves) // or added in the ExportOptions. public delegate string RetrieveTexturePathDelegate(Texture texture); public delegate void BeforeSceneExportDelegate(GLTFSceneExporter exporter, GLTFRoot gltfRoot); public delegate void AfterSceneExportDelegate(GLTFSceneExporter exporter, GLTFRoot gltfRoot); public delegate void AfterNodeExportDelegate(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Transform transform, Node node); /// True: material export is complete. False: continue regular export. public delegate bool BeforeMaterialExportDelegate(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Material material, GLTFMaterial materialNode); public delegate void AfterMaterialExportDelegate(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Material material, GLTFMaterial materialNode); public delegate void BeforeTextureExportDelegate(GLTFSceneExporter exporter, ref UniqueTexture texture, string textureSlot); public delegate void AfterTextureExportDelegate(GLTFSceneExporter exporter, UniqueTexture texture, int index, GLTFTexture tex); public delegate void AfterPrimitiveExportDelegate(GLTFSceneExporter exporter, Mesh mesh, MeshPrimitive primitive, int index); public delegate void AfterMeshExportDelegate(GLTFSceneExporter exporter, Mesh mesh, GLTFMesh gltfMesh, int index); private class LegacyCallbacksPlugin : GLTFExportPluginContext { public override void AfterSceneExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot) => GLTFSceneExporter.AfterSceneExport?.Invoke(exporter, gltfRoot); public override void BeforeSceneExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot) => GLTFSceneExporter.BeforeSceneExport?.Invoke(exporter, gltfRoot); public override void AfterNodeExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Transform transform, Node node) => GLTFSceneExporter.AfterNodeExport?.Invoke(exporter, gltfRoot, transform, node); public override bool BeforeMaterialExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Material material, GLTFMaterial materialNode) { // static callback, run after options callback // we're iterating here because we want to stop calling any once we hit one that can export this material. if (GLTFSceneExporter.BeforeMaterialExport != null) { var list = GLTFSceneExporter.BeforeMaterialExport.GetInvocationList(); foreach (var entry in list) { var cb = (BeforeMaterialExportDelegate) entry; if (cb != null && cb.Invoke(exporter, gltfRoot, material, materialNode)) { return true; } } } return false; } public override void AfterMaterialExport(GLTFSceneExporter exporter, GLTFRoot gltfRoot, Material material, GLTFMaterial materialNode) => GLTFSceneExporter.AfterMaterialExport?.Invoke(exporter, gltfRoot, material, materialNode); } private static ILogger Debug = UnityEngine.Debug.unityLogger; private List _plugins = new List(); public IReadOnlyList Plugins => _plugins; public struct TextureMapType { public const string BaseColor = "baseColorTexture"; [Obsolete("Use BaseColor instead")] public const string Main = BaseColor; public const string Emissive = "emissiveTexture"; [Obsolete("Use Emissive instead")] public const string Emission = Emissive; public const string Normal = "normalTexture"; [Obsolete("Use Normal instead")] public const string Bump = Normal; public const string MetallicGloss = "metallicGloss"; public const string MetallicRoughness = "metallicRoughnessTexture"; public const string SpecGloss = "specularGlossinessTexture"; // not really supported anymore public const string Light = Linear; public const string Occlusion = "occlusionTexture"; public const string Linear = "linear"; public const string sRGB = "sRGB"; public const string Custom_Unknown = "linearWithAlpha"; public const string Custom_HDR = "hdr"; [Obsolete("Use Linear or the right texture slot instead")] public const string MetallicGloss_DontConvert = Linear; } public struct TextureExportSettings { public bool isValid; // does the texture need a channel conversion when exporting public Conversion conversion; // do we know something about the alpha channel of this texture public AlphaMode alphaMode; // is the texture linear or sRGB public bool linear; // required for metallic-smoothness conversion public float smoothnessRangeMin; public float smoothnessRangeMax; public float metallicRangeMin; public float metallicRangeMax; public float occlusionRangeMin; public float occlusionRangeMax; public TextureExportSettings(TextureExportSettings source) { conversion = source.conversion; alphaMode = source.alphaMode; linear = source.linear; smoothnessRangeMin = source.smoothnessRangeMin; smoothnessRangeMax = source.smoothnessRangeMax; metallicRangeMin = source.metallicRangeMin; metallicRangeMax = source.metallicRangeMax; occlusionRangeMin = source.occlusionRangeMin; occlusionRangeMax = source.occlusionRangeMax; isValid = true; } public enum Conversion { None, MetalGlossChannelSwap, MetalGlossOcclusionChannelSwap, NormalChannel, } public enum AlphaMode { Never = 0, Always = 1, Heuristic = 2, } public static bool operator ==(TextureExportSettings lhs, TextureExportSettings rhs) { return lhs.Equals(rhs); } public static bool operator !=(TextureExportSettings lhs, TextureExportSettings rhs) { return !(lhs == rhs); } public bool Equals(TextureExportSettings other) { return conversion == other.conversion && alphaMode == other.alphaMode && linear == other.linear && Mathf.Approximately(smoothnessRangeMin, other.smoothnessRangeMin) && Mathf.Approximately(smoothnessRangeMax, other.smoothnessRangeMax) && Mathf.Approximately(metallicRangeMin, other.metallicRangeMin) && Mathf.Approximately(metallicRangeMax, other.metallicRangeMax) && Mathf.Approximately(occlusionRangeMin, other.occlusionRangeMin) && Mathf.Approximately(occlusionRangeMax, other.occlusionRangeMax); } public override bool Equals(object obj) { return obj is TextureExportSettings other && Equals(other); } public override int GetHashCode() { unchecked { var hashCode = (int)conversion; hashCode = (hashCode * 397) ^ (int)alphaMode; hashCode = (hashCode * 397) ^ linear.GetHashCode(); hashCode = (hashCode * 397) ^ smoothnessRangeMin.GetHashCode(); hashCode = (hashCode * 397) ^ smoothnessRangeMax.GetHashCode(); hashCode = (hashCode * 397) ^ metallicRangeMin.GetHashCode(); hashCode = (hashCode * 397) ^ metallicRangeMax.GetHashCode(); hashCode = (hashCode * 397) ^ occlusionRangeMin.GetHashCode(); hashCode = (hashCode * 397) ^ occlusionRangeMax.GetHashCode(); return hashCode; } } } public TextureExportSettings GetExportSettingsForSlot(string textureSlot) { var exportSettings = new TextureExportSettings(); exportSettings.isValid = true; exportSettings.metallicRangeMin = 0f; exportSettings.metallicRangeMax = 1f; exportSettings.smoothnessRangeMin = 0f; exportSettings.smoothnessRangeMax = 1f; exportSettings.occlusionRangeMin = 0f; exportSettings.occlusionRangeMax = 1f; switch (textureSlot) { case TextureMapType.BaseColor: // Main = new TextureExportSettings() { alphaMode = AlphaMode.Heuristic }; exportSettings.linear = false; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Heuristic; return exportSettings; case TextureMapType.Emissive: // Emission = new TextureExportSettings() { alphaMode = AlphaMode.Heuristic }; exportSettings.linear = false; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Never; return exportSettings; case TextureMapType.Normal: // Bump = new TextureExportSettings() { alphaMode = AlphaMode.Never, conversion = Conversion.NormalChannel }; exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Never; exportSettings.conversion = TextureExportSettings.Conversion.NormalChannel; return exportSettings; case TextureMapType.MetallicGloss: // MetallicGloss = new TextureExportSettings() { alphaMode = AlphaMode.Never, conversion = Conversion.MetalGlossChannelSwap, smoothnessMultiplier = 1f}; exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Never; exportSettings.conversion = TextureExportSettings.Conversion.MetalGlossChannelSwap; return exportSettings; case TextureMapType.MetallicRoughness: exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Never; return exportSettings; case TextureMapType.SpecGloss: // SpecGloss = MetallicGloss; // not really supported anymore exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Never; exportSettings.conversion = TextureExportSettings.Conversion.MetalGlossChannelSwap; return exportSettings; case TextureMapType.Occlusion: // Occlusion = Linear; exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Never; return exportSettings; // custom slot types that allow us to export more arbitrary textures case TextureMapType.Linear: // MetallicGloss_DontConvert = Linear; exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Heuristic; return exportSettings; case TextureMapType.sRGB: // MetallicGloss_DontConvert = Linear; exportSettings.linear = false; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Heuristic; return exportSettings; case TextureMapType.Custom_Unknown: case "rgbm": // Custom_Unknown = new TextureExportSettings() { linear = true, alphaMode = AlphaMode.Always }; exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Always; return exportSettings; case TextureMapType.Custom_HDR: // Custom_HDR = new TextureExportSettings() { alphaMode = AlphaMode.Always }; exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Always; return exportSettings; } // assume unknown linear exportSettings.linear = true; exportSettings.alphaMode = TextureExportSettings.AlphaMode.Heuristic; return exportSettings; } private Material GetConversionMaterial(TextureExportSettings textureMapType) { switch (textureMapType.conversion) { case TextureExportSettings.Conversion.NormalChannel: return _normalChannelMaterial; case TextureExportSettings.Conversion.MetalGlossChannelSwap: return SetConversionMaterialSettings(_metalGlossChannelSwapMaterial, textureMapType); case TextureExportSettings.Conversion.MetalGlossOcclusionChannelSwap: return SetConversionMaterialSettings(_metalGlossOcclusionChannelSwapMaterial, textureMapType); default: return null; } } private static Material SetConversionMaterialSettings(Material material, TextureExportSettings textureMapType) { if (material && material.HasProperty("_SmoothnessRangeMin")) material.SetFloat("_SmoothnessRangeMin", textureMapType.smoothnessRangeMin); if (material && material.HasProperty("_SmoothnessRangeMax")) material.SetFloat("_SmoothnessRangeMax", textureMapType.smoothnessRangeMax); if (material && material.HasProperty("_MetallicRangeMin")) material.SetFloat("_MetallicRangeMin", textureMapType.metallicRangeMin); if (material && material.HasProperty("_MetallicRangeMax")) material.SetFloat("_MetallicRangeMax", textureMapType.metallicRangeMax); if (material && material.HasProperty("_OcclusionRangeMin")) material.SetFloat("_OcclusionRangeMin", textureMapType.occlusionRangeMin); if (material && material.HasProperty("_OcclusionRangeMax")) material.SetFloat("_OcclusionRangeMax", textureMapType.occlusionRangeMax); return material; } private struct ImageInfo { public Texture2D texture; public TextureExportSettings textureMapType; public string outputPath; public bool canBeExportedFromDisk; } private struct FileInfo { public Stream stream; public string uniqueFileName; } public struct ExportFileResult { public string uri; public string mimeType; public BufferViewId bufferView; } public IReadOnlyList RootTransforms => _rootTransforms; private Transform[] _rootTransforms; private GLTFRoot _root; private BufferId _bufferId; private GLTFBuffer _buffer; private List _imageInfos; private HashSet _imageExportPaths; private List _fileInfos; private HashSet _fileNames; private List _textures; private Dictionary _exportedMaterials; private bool _shouldUseInternalBufferForImages; private Dictionary _exportedTransforms; private List _animatedNodes; private int _exportLayerMask; private ExportContext _exportContext; private Material _metalGlossChannelSwapMaterial; private Material _metalGlossOcclusionChannelSwapMaterial; private Material _normalChannelMaterial; private const uint MagicGLTF = 0x46546C67; private const uint Version = 2; private const uint MagicJson = 0x4E4F534A; private const uint MagicBin = 0x004E4942; private const int GLTFHeaderSize = 12; private const int SectionHeaderSize = 8; private bool _visbilityPluginEnabled = false; public struct UniqueTexture : IEquatable { public Texture Texture; public int MaxSize; // additional settings that make exporting a texture unique public TextureExportSettings ExportSettings; public int GetWidth() => Mathf.Min(MaxSize, Texture.width); public int GetHeight() => Mathf.Min(MaxSize, Texture.height); public UniqueTexture(Texture tex, string textureSlot, GLTFSceneExporter exporter) { Texture = tex; ExportSettings = exporter.GetExportSettingsForSlot(textureSlot); MaxSize = Mathf.Max(tex.width, tex.height); } public UniqueTexture(Texture tex, TextureExportSettings exportSettings) { Texture = tex; ExportSettings = exportSettings; MaxSize = Mathf.Max(tex.width, tex.height); } public bool Equals(UniqueTexture other) { return Equals(Texture, other.Texture) && MaxSize == other.MaxSize && ExportSettings == other.ExportSettings; } public override bool Equals(object obj) { return obj is UniqueTexture other && Equals(other); } public override int GetHashCode() { unchecked { // We dont want to use GetHashCode() for the texture here since it will change the hash after restarting the editor #if UNITY_EDITOR var hashCode = 0; if (Texture && Texture.imageContentsHash.isValid) hashCode = Texture.imageContentsHash.GetHashCode(); else if (Texture) hashCode = Texture.GetHashCode(); #else var hashCode = Texture ? Texture.GetHashCode() : 0; #endif hashCode = (hashCode * 397) ^ ExportSettings.GetHashCode(); hashCode = (hashCode * 397) ^ MaxSize; return hashCode; } } } /// /// A Primitive is a combination of Mesh + Material(s). It also contains a reference to the original SkinnedMeshRenderer, /// if any, since that's the only way to get the actual current weights to export a blend shape primitive. /// public struct UniquePrimitive { public bool Equals(UniquePrimitive other) { if (!Equals(Mesh, other.Mesh)) return false; if (Materials == null && other.Materials == null) return true; if (!(Materials != null && other.Materials != null)) return false; if (!Equals(Materials.Length, other.Materials.Length)) return false; for (var i = 0; i < Materials.Length; i++) { if (!Equals(Materials[i], other.Materials[i])) return false; } return true; } public override bool Equals(object obj) { return obj is UniquePrimitive other && Equals(other); } public override int GetHashCode() { unchecked { var code = (Mesh != null ? Mesh.GetHashCode() : 0) * 397; if (Materials != null) { code = code ^ Materials.Length.GetHashCode() * 397; foreach (var mat in Materials) code = (code ^ (mat != null ? mat.GetHashCode() : 0)) * 397; } return code; } } public Mesh Mesh; public Material[] Materials; public SkinnedMeshRenderer SkinnedMeshRenderer; // needed for BlendShape export, since Unity stores the actually used blend shape weights on the renderer. see ExporterMeshes.ExportBlendShapes } private readonly Dictionary _primOwner = new Dictionary(); #region Settings private GLTFSettings settings => _exportContext.settings; private bool ExportNames => settings.ExportNames; private bool ExportAnimations => settings.ExportAnimations; #endregion #region Profiler Markers // ReSharper disable InconsistentNaming private static ProfilerMarker exportGltfMarker = new ProfilerMarker("Export glTF"); private static ProfilerMarker gltfSerializationMarker = new ProfilerMarker("Serialize exported data"); private static ProfilerMarker exportMeshMarker = new ProfilerMarker("Export Mesh"); private static ProfilerMarker exportPrimitiveMarker = new ProfilerMarker("Export Primitive"); private static ProfilerMarker exportBlendShapeMarker = new ProfilerMarker("Export BlendShape"); private static ProfilerMarker exportSkinFromNodeMarker = new ProfilerMarker("Export Skin"); private static ProfilerMarker exportSparseAccessorMarker = new ProfilerMarker("Export Sparse Accessor"); private static ProfilerMarker beforeNodeExportMarker = new ProfilerMarker("Before Node Export (Callback)"); private static ProfilerMarker exportNodeMarker = new ProfilerMarker("Export Node"); private static ProfilerMarker afterNodeExportMarker = new ProfilerMarker("After Node Export (Callback)"); private static ProfilerMarker exportAnimationFromNodeMarker = new ProfilerMarker("Export Animation from Node"); private static ProfilerMarker convertClipToGLTFAnimationMarker = new ProfilerMarker("Convert Clip to GLTF Animation"); private static ProfilerMarker beforeSceneExportMarker = new ProfilerMarker("Before Scene Export (Callback)"); private static ProfilerMarker exportSceneMarker = new ProfilerMarker("Export Scene"); private static ProfilerMarker beforeMaterialExportMarker = new ProfilerMarker("Before Material Export (Callback)"); private static ProfilerMarker exportMaterialMarker = new ProfilerMarker("Export Material"); private static ProfilerMarker afterMaterialExportMarker = new ProfilerMarker("After Material Export (Callback)"); private static ProfilerMarker writeImageToDiskMarker = new ProfilerMarker("Export Image - Write to Disk"); private static ProfilerMarker afterSceneExportMarker = new ProfilerMarker("After Scene Export (Callback)"); private static ProfilerMarker exportAccessorMarker = new ProfilerMarker("Export Accessor"); private static ProfilerMarker exportAccessorMatrix4x4ArrayMarker = new ProfilerMarker("Matrix4x4[]"); private static ProfilerMarker exportAccessorVector4ArrayMarker = new ProfilerMarker("Vector4[]"); private static ProfilerMarker exportAccessorUintArrayMarker = new ProfilerMarker("Uint[]"); private static ProfilerMarker exportAccessorColorArrayMarker = new ProfilerMarker("Color[]"); private static ProfilerMarker exportAccessorVector3ArrayMarker = new ProfilerMarker("Vector3[]"); private static ProfilerMarker exportAccessorVector2ArrayMarker = new ProfilerMarker("Vector2[]"); private static ProfilerMarker exportAccessorIntArrayIndicesMarker = new ProfilerMarker("int[] (Indices)"); private static ProfilerMarker exportAccessorIntArrayMarker = new ProfilerMarker("int[]"); private static ProfilerMarker exportAccessorFloatArrayMarker = new ProfilerMarker("float[]"); private static ProfilerMarker exportAccessorByteArrayMarker = new ProfilerMarker("byte[]"); private static ProfilerMarker exportAccessorMinMaxMarker = new ProfilerMarker("Calculate min/max"); private static ProfilerMarker exportAccessorBufferWriteMarker = new ProfilerMarker("Buffer.Write"); private static ProfilerMarker exportGltfInitMarker = new ProfilerMarker("Init glTF Export"); private static ProfilerMarker gltfWriteOutMarker = new ProfilerMarker("Write glTF"); private static ProfilerMarker gltfWriteJsonStreamMarker = new ProfilerMarker("Write JSON stream"); private static ProfilerMarker gltfWriteBinaryStreamMarker = new ProfilerMarker("Write binary stream"); private static ProfilerMarker addAnimationDataMarker = new ProfilerMarker("Add animation data to glTF"); private static ProfilerMarker exportRotationAnimationDataMarker = new ProfilerMarker("Rotation Keyframes"); private static ProfilerMarker exportPositionAnimationDataMarker = new ProfilerMarker("Position Keyframes"); private static ProfilerMarker exportScaleAnimationDataMarker = new ProfilerMarker("Scale Keyframes"); private static ProfilerMarker exportWeightsAnimationDataMarker = new ProfilerMarker("Weights Keyframes"); private static ProfilerMarker removeAnimationUnneededKeyframesMarker = new ProfilerMarker("Simplify Keyframes"); private static ProfilerMarker removeAnimationUnneededKeyframesInitMarker = new ProfilerMarker("Init"); private static ProfilerMarker removeAnimationUnneededKeyframesCheckIdenticalMarker = new ProfilerMarker("Check Identical"); private static ProfilerMarker removeAnimationUnneededKeyframesCheckIdenticalKeepMarker = new ProfilerMarker("Keep Keyframe"); private static ProfilerMarker removeAnimationUnneededKeyframesFinalizeMarker = new ProfilerMarker("Finalize"); // ReSharper restore InconsistentNaming #endregion /// /// Create a GLTFExporter that exports out a transform /// /// Root transform of object to export [Obsolete("Please switch to GLTFSceneExporter(Transform[] rootTransforms, ExportOptions options). This constructor is deprecated and will be removed in a future release.")] public GLTFSceneExporter(Transform[] rootTransforms, RetrieveTexturePathDelegate texturePathRetriever) : this(rootTransforms, new ExportContext { TexturePathRetriever = texturePathRetriever }) { } public GLTFSceneExporter(Transform rootTransform, ExportContext context) : this(new [] { rootTransform }, context) { } /// /// Create a GLTFExporter that exports out a transform /// /// Root transform of object to export /// Export Settings public GLTFSceneExporter(Transform[] rootTransforms, ExportContext context) { _exportContext = context; if (context.logger != null) Debug = context.logger; else Debug = UnityEngine.Debug.unityLogger; // legacy: implicit plugin for all the static methods on GLTFSceneExporter _plugins.Add(new LegacyCallbacksPlugin()); // legacy: implicit plugin for all the methods on ExportContext _plugins.Add(context.GetExportContextCallbacks()); // create export plugin instances foreach (var plugin in settings.ExportPlugins) { if (plugin != null && plugin.Enabled) { var instance = plugin.CreateInstance(context); if (instance != null) _plugins.Add(instance); } } _exportLayerMask = _exportContext.ExportLayers; var metalGlossChannelSwapShader = Resources.Load("MetalGlossChannelSwap", typeof(Shader)) as Shader; _metalGlossChannelSwapMaterial = new Material(metalGlossChannelSwapShader); var metalGlossOcclusionChannelSwapShader = Resources.Load("MetalGlossOcclusionChannelSwap", typeof(Shader)) as Shader; _metalGlossOcclusionChannelSwapMaterial = new Material(metalGlossOcclusionChannelSwapShader); var normalChannelShader = Resources.Load("NormalChannel", typeof(Shader)) as Shader; _normalChannelMaterial = new Material(normalChannelShader); // Remove invalid transforms _rootTransforms = rootTransforms?.Where(x => x).ToArray() ?? Array.Empty(); _exportedTransforms = new Dictionary(); _exportedCameras = new Dictionary(); _exportedLights = new Dictionary(); _animatedNodes = new List(); _skinnedNodes = new List(); _bakedMeshes = new Dictionary(); _root = new GLTFRoot { Accessors = new List(), Animations = new List(), Asset = new Asset { Version = "2.0", Generator = settings.Generator }, Buffers = new List(), BufferViews = new List(), Cameras = new List(), Images = new List(), Materials = new List(), Meshes = new List(), Nodes = new List(), Samplers = new List(), Scenes = new List(), Skins = new List(), Textures = new List() }; _imageInfos = new List(); _fileInfos = new List(); _fileNames = new HashSet(); _exportedMaterials = new Dictionary(); _textures = new List(); _imageExportPaths = new HashSet(); _buffer = new GLTFBuffer(); _bufferId = new BufferId { Id = _root.Buffers.Count, Root = _root }; _root.Buffers.Add(_buffer); foreach (var plugin in settings.ExportPlugins) { if (plugin != null && plugin.Enabled && plugin.AssetExtras != null) _root.Asset.PluginExtras.Add(plugin.DisplayName, plugin.AssetExtras); } _visbilityPluginEnabled = settings.ExportPlugins.Any(x => x is VisibilityExport && x.Enabled); if (_visbilityPluginEnabled && !settings.ExportDisabledGameObjects) { Debug.Log(LogType.Warning,"KHR_node_visibility export plugin is enabled, but Export Disabled GameObjects is not. This may lead to unexpected results."); } } /// /// Gets the root object of the exported GLTF /// /// Root parsed GLTF Json public GLTFRoot GetRoot() { return _root; } /// /// Writes a binary GLB file with filename at path. /// /// File path for saving the binary file /// The name of the GLTF file public void SaveGLB(string path, string fileName) { var fullPath = GetFileName(path, fileName, ".glb"); var dirName = Path.GetDirectoryName(fullPath); if (dirName != null && !Directory.Exists(dirName)) Directory.CreateDirectory(dirName); _shouldUseInternalBufferForImages = true; using (FileStream glbFile = new FileStream(fullPath, FileMode.Create)) { SaveGLBToStream(glbFile, fileName); } if (!_shouldUseInternalBufferForImages) { ExportImages(path); ExportFiles(path); } } /// /// In-memory GLB creation helper. Useful for platforms where no filesystem is available (e.g. WebGL). /// /// /// public byte[] SaveGLBToByteArray(string sceneName) { _shouldUseInternalBufferForImages = true; using (var stream = new MemoryStream()) { SaveGLBToStream(stream, sceneName); return stream.ToArray(); } } /// /// Writes a binary GLB file into a stream (memory stream, filestream, ...) /// /// File path for saving the binary file /// The name of the GLTF file public void SaveGLBToStream(Stream stream, string sceneName) { exportGltfMarker.Begin(); exportGltfInitMarker.Begin(); Stream binStream = new MemoryStream(); Stream jsonStream = new MemoryStream(); _shouldUseInternalBufferForImages = true; _bufferWriter = new BinaryWriterWithLessAllocations(binStream); TextWriter jsonWriter = new StreamWriter(jsonStream, new UTF8Encoding(false)); exportGltfInitMarker.End(); beforeSceneExportMarker.Begin(); foreach (var plugin in _plugins) plugin?.BeforeSceneExport(this, _root); beforeSceneExportMarker.End(); _root.Scene = ExportScene(sceneName, _rootTransforms); if (ExportAnimations) { ExportAnimation(); } // Export skins for (int i = 0; i < _skinnedNodes.Count; ++i) { Transform t = _skinnedNodes[i]; ExportSkinFromNode(t); } afterSceneExportMarker.Begin(); foreach (var plugin in _plugins) plugin?.AfterSceneExport(this, _root); afterSceneExportMarker.End(); animationPointerResolver?.Resolve(this); _buffer.ByteLength = CalculateAlignment((uint)_bufferWriter.BaseStream.Length, 4); gltfSerializationMarker.Begin(); _root.Serialize(jsonWriter, true); gltfSerializationMarker.End(); gltfWriteOutMarker.Begin(); _bufferWriter.Flush(); jsonWriter.Flush(); // align to 4-byte boundary to comply with spec. AlignToBoundary(jsonStream); AlignToBoundary(binStream, 0x00); int glbLength = (int)(GLTFHeaderSize + SectionHeaderSize + jsonStream.Length + SectionHeaderSize + binStream.Length); BinaryWriter writer = new BinaryWriter(stream); // write header writer.Write(MagicGLTF); writer.Write(Version); writer.Write(glbLength); gltfWriteJsonStreamMarker.Begin(); // write JSON chunk header. writer.Write((int)jsonStream.Length); writer.Write(MagicJson); jsonStream.Position = 0; CopyStream(jsonStream, writer); gltfWriteJsonStreamMarker.End(); gltfWriteBinaryStreamMarker.Begin(); writer.Write((int)binStream.Length); writer.Write(MagicBin); binStream.Position = 0; CopyStream(binStream, writer); gltfWriteBinaryStreamMarker.End(); writer.Flush(); gltfWriteOutMarker.End(); exportGltfMarker.End(); } /// /// Specifies the path and filename for the GLTF Json and binary /// /// File path for saving the GLTF and binary files /// The name of the GLTF file public void SaveGLTFandBin(string path, string fileName, bool exportTextures = true) { exportGltfMarker.Begin(); exportGltfInitMarker.Begin(); _shouldUseInternalBufferForImages = false; var toLower = fileName.ToLowerInvariant(); if (toLower.EndsWith(".gltf")) fileName = fileName.Substring(0, fileName.Length - 5); if (toLower.EndsWith(".bin")) fileName = fileName.Substring(0, fileName.Length - 4); var fullPath = GetFileName(path, fileName, ".bin"); var dirName = Path.GetDirectoryName(fullPath); if (dirName != null && !Directory.Exists(dirName)) Directory.CreateDirectory(dirName); // sanitized file path can differ fileName = Path.GetFileNameWithoutExtension(fullPath); var binFile = File.Create(fullPath); _bufferWriter = new BinaryWriterWithLessAllocations(binFile); exportGltfInitMarker.End(); beforeSceneExportMarker.Begin(); foreach (var plugin in _plugins) plugin?.BeforeSceneExport(this, _root); beforeSceneExportMarker.End(); if (_rootTransforms != null) _root.Scene = ExportScene(fileName, _rootTransforms); if (ExportAnimations) ExportAnimation(); // Export skins for (int i = 0; i < _skinnedNodes.Count; ++i) { Transform t = _skinnedNodes[i]; ExportSkinFromNode(t); } afterSceneExportMarker.Begin(); foreach (var plugin in _plugins) plugin?.AfterSceneExport(this, _root); afterSceneExportMarker.End(); animationPointerResolver?.Resolve(this); // we don't need to create a .bin file if there's no buffer at all var anyDataInBinFile = _bufferWriter.BaseStream.Length > 0; if (anyDataInBinFile) { AlignToBoundary(_bufferWriter.BaseStream, 0x00); _buffer.Uri = fileName + ".bin"; _buffer.ByteLength = CalculateAlignment((uint)_bufferWriter.BaseStream.Length, 4); } else { _buffer = null; _root.Buffers.Clear(); } var gltfFile = File.CreateText(Path.ChangeExtension(fullPath, ".gltf")); gltfSerializationMarker.Begin(); _root.Serialize(gltfFile); gltfSerializationMarker.End(); gltfWriteOutMarker.Begin(); _bufferWriter.Close(); #if WINDOWS_UWP gltfFile.Dispose(); binFile.Dispose(); #else gltfFile.Close(); binFile.Close(); #endif if (!anyDataInBinFile) File.Delete(fullPath); if (exportTextures) ExportImages(path); ExportFiles(path); gltfWriteOutMarker.End(); exportGltfMarker.End(); } /// /// Ensures a specific file extension from an absolute path that may or may not already have that extension. /// /// Absolute path that may or may not already have the required extension /// The extension to ensure, with leading dot /// An absolute path that has the required extension public static string GetFileName(string directory, string fileNameThatMayHaveExtension, string requiredExtension) { var absolutePathThatMayHaveExtension = Path.Combine(directory, EnsureValidFileName(fileNameThatMayHaveExtension)); if (!requiredExtension.StartsWith(".", StringComparison.Ordinal)) requiredExtension = "." + requiredExtension; if (!Path.GetExtension(absolutePathThatMayHaveExtension).Equals(requiredExtension, StringComparison.OrdinalIgnoreCase)) return absolutePathThatMayHaveExtension + requiredExtension; return absolutePathThatMayHaveExtension; } /// /// Strip illegal chars and reserved words from a candidate filename (should not include the directory path) /// /// /// http://stackoverflow.com/questions/309485/c-sharp-sanitize-file-name /// private static string EnsureValidFileName(string filename) { if (filename == null) return ""; var invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars())); var invalidReStr = string.Format(@"[{0}]+", invalidChars); var reservedWords = new [] { "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; var sanitisedNamePart = Regex.Replace(filename, invalidReStr, "_"); foreach (var reservedWord in reservedWords) { var reservedWordPattern = string.Format("^{0}\\.", reservedWord); sanitisedNamePart = Regex.Replace(sanitisedNamePart, reservedWordPattern, "_reservedWord_.", RegexOptions.IgnoreCase); } return sanitisedNamePart; } public void DeclareExtensionUsage(string extension, bool isRequired=false) { if( _root.ExtensionsUsed == null ){ _root.ExtensionsUsed = new List(); } if(!_root.ExtensionsUsed.Contains(extension)) { _root.ExtensionsUsed.Add(extension); } if(isRequired){ if( _root.ExtensionsRequired == null ){ _root.ExtensionsRequired = new List(); } if( !_root.ExtensionsRequired.Contains(extension)) { _root.ExtensionsRequired.Add(extension); } } } private bool ShouldExportTransform(Transform transform) { // Root transforms should *always* be exported since this is a deliberate decision by the user calling - it should override any other setting that would prevent the export (e.g. if a user calls Export with disabled or hidden objects the exporter should never prevent this) var isRoot = _rootTransforms.Contains(transform); if (isRoot) return true; if (!settings.ExportDisabledGameObjects && !transform.gameObject.activeSelf) { return false; } if (settings.UseMainCameraVisibility && (_exportLayerMask >= 0 && _exportLayerMask != (_exportLayerMask | 1 << transform.gameObject.layer))) return false; if (transform.CompareTag("EditorOnly")) return false; return true; } private SceneId ExportScene(string name, Transform[] rootObjTransforms) { if (rootObjTransforms == null || rootObjTransforms.Length < 1) return null; exportSceneMarker.Begin(); var scene = new GLTFScene(); if (ExportNames) { scene.Name = name; } if(_exportContext.TreatEmptyRootAsScene) { // if we're exporting with a single object selected, that object can be the scene root, no need for an extra root node. if (rootObjTransforms.Length == 1 && rootObjTransforms[0].GetComponents().Length == 1) // single root with a single transform { var firstRoot = rootObjTransforms[0]; var newRoots = new Transform[firstRoot.childCount]; for (int i = 0; i < firstRoot.childCount; i++) newRoots[i] = firstRoot.GetChild(i); rootObjTransforms = newRoots; } } scene.Nodes = new List(rootObjTransforms.Length); foreach (var transform in rootObjTransforms) { if (!transform) { Debug.LogWarning("GLTFSceneExporter", $"Skipping empty transform in root transforms provided for scene export for {name}", transform); continue; } scene.Nodes.Add(ExportNode(transform)); } _root.Scenes.Add(scene); exportSceneMarker.End(); return new SceneId { Id = _root.Scenes.Count - 1, Root = _root }; } private NodeId ExportNode(Transform nodeTransform) { if (_exportedTransforms.TryGetValue(nodeTransform.GetInstanceID(), out var existingNodeId)) return new NodeId() { Id = existingNodeId, Root = _root }; foreach (var plugin in _plugins) if (!(plugin?.ShouldNodeExport(this, _root, nodeTransform) ?? true)) return null; exportNodeMarker.Begin(); var node = new Node(); if (_visbilityPluginEnabled && !nodeTransform.gameObject.activeSelf) { DeclareExtensionUsage(KHR_node_visibility_Factory.EXTENSION_NAME, false); node.AddExtension(KHR_node_visibility_Factory.EXTENSION_NAME, new KHR_node_visibility { visible = false }); } if (ExportNames) { node.Name = nodeTransform.name; } // TODO think more about how this callback is used – could e.g. be modifying the hierarchy, // and we would want to prevent exporting children of this node. // Could also be that we want to add a mesh based on some condition // (e.g. merged childs, procedural geometry, etc.) beforeNodeExportMarker.Begin(); foreach (var plugin in _plugins) plugin?.BeforeNodeExport(this, _root, nodeTransform, node); beforeNodeExportMarker.End(); #if ANIMATION_SUPPORTED if (nodeTransform.GetComponent() || nodeTransform.GetComponent()) { _animatedNodes.Add(nodeTransform); } #endif if (nodeTransform.GetComponent() && ContainsValidRenderer(nodeTransform.gameObject, settings.ExportDisabledGameObjects)) { _skinnedNodes.Add(nodeTransform); } // export camera attached to node Camera unityCamera = nodeTransform.GetComponent(); if (unityCamera != null && unityCamera.enabled) { node.Camera = ExportCamera(unityCamera); } var lightPluginEnabled = _plugins.FirstOrDefault(x => x is LightsPunctualExportContext) != null; Light unityLight = nodeTransform.GetComponent(); if (unityLight != null && unityLight.enabled && lightPluginEnabled) { node.Light = ExportLight(unityLight); } var needsInvertedLookDirection = unityLight || unityCamera; if (needsInvertedLookDirection) { node.SetUnityTransform(nodeTransform, true); } else { node.SetUnityTransform(nodeTransform, false); } var id = new NodeId { Id = _root.Nodes.Count, Root = _root }; // Register nodes for animation parsing (could be disabled if animation is disabled) _exportedTransforms.Add(nodeTransform.GetInstanceID(), _root.Nodes.Count); _root.Nodes.Add(node); // children that are primitives get put in a mesh FilterPrimitives(nodeTransform, out GameObject[] primitives, out GameObject[] nonPrimitives); if (primitives.Length > 0) { var uniquePrimitives = GetUniquePrimitivesFromGameObjects(primitives); if (uniquePrimitives != null) { node.Mesh = ExportMesh(nodeTransform.name, uniquePrimitives); RegisterPrimitivesWithNode(node, uniquePrimitives); // Node - BlendShape Weights if (uniquePrimitives[0].SkinnedMeshRenderer) { var meshObj = uniquePrimitives[0].Mesh; var smr = uniquePrimitives[0].SkinnedMeshRenderer; // Only export the blendShapeWeights into the Node, when it's not the first SkinnedMeshRenderer with the same Mesh // Because the weights already exported into the GltfMesh if (smr && meshObj && _meshToBlendShapeAccessors.TryGetValue(meshObj, out var data) && smr != data.firstSkinnedMeshRenderer) { var blendShapeWeights = GetBlendShapeWeights(smr, meshObj); if (blendShapeWeights != null) { if (data.weights != null) { // Check if the blendShapeWeights has any differences to the weights already exported into the gltfMesh // When not, we don't need to set the same values to the Node Weights bool isSame = true; for (int i = 0; i < blendShapeWeights.Count; i++) isSame &= System.Math.Abs(blendShapeWeights[i] - data.weights[i]) < double.Epsilon; if (!isSame) node.Weights = blendShapeWeights; } } } } } } exportNodeMarker.End(); // children that are not primitives get added as child nodes if (nonPrimitives.Length > 0) { var parentOfChilds = node; // when we're exporting a light or camera, we add an implicit node as first child of the camera/light node. // this ensures that child objects and animations etc. "just work". if (needsInvertedLookDirection) { var inbetween = new Node(); if (ExportNames) { inbetween.Name = nodeTransform.name + "-flipped"; } inbetween.Rotation = Quaternion.Inverse(SchemaExtensions.InvertDirection).ToGltfQuaternionConvert(); var inbetweenId = new NodeId { Id = _root.Nodes.Count, Root = _root }; _root.Nodes.Add(inbetween); node.Children = new List(1); node.Children.Add(inbetweenId); parentOfChilds = inbetween; } parentOfChilds.Children = new List(nonPrimitives.Length); foreach (var child in nonPrimitives) { if (!ShouldExportTransform(child.transform)) continue; var childNode = ExportNode(child.transform); if (childNode != null) parentOfChilds.Children.Add(childNode); } } // node export callback afterNodeExportMarker.Begin(); foreach (var plugin in _plugins) plugin?.AfterNodeExport(this, _root, nodeTransform, node); afterNodeExportMarker.End(); return id; } private static bool ContainsValidRenderer(GameObject gameObject, bool exportDisabledGameObjects) { if (!gameObject) return false; var meshRenderer = gameObject.GetComponent(); var meshFilter = gameObject.GetComponent(); var skinnedMeshRender = gameObject.GetComponent(); var materials = meshRenderer ? meshRenderer.sharedMaterials : skinnedMeshRender ? skinnedMeshRender.sharedMaterials : null; var anyMaterialIsNonNull = false; if (materials != null) for (int i = 0; i < materials.Length; i++) anyMaterialIsNonNull |= materials[i]; return ((meshFilter && meshRenderer && (meshRenderer.enabled || exportDisabledGameObjects)) || (skinnedMeshRender && (skinnedMeshRender.enabled || exportDisabledGameObjects))) && anyMaterialIsNonNull; } private void FilterPrimitives(Transform transform, out GameObject[] primitives, out GameObject[] nonPrimitives) { var childCount = transform.childCount; var prims = new List(childCount + 1); var nonPrims = new List(childCount); // add another primitive if the root object also has a mesh if (ShouldExportTransform(transform)) { if (ContainsValidRenderer(transform.gameObject, settings.ExportDisabledGameObjects)) { prims.Add(transform.gameObject); } } for (var i = 0; i < childCount; i++) { var go = transform.GetChild(i).gameObject; // This seems to be a performance optimization but results in transforms that are detected as "primitives" not being animated // if (IsPrimitive(go)) // prims.Add(go); // else nonPrims.Add(go); } primitives = prims.ToArray(); nonPrimitives = nonPrims.ToArray(); } // This seems to be a performance optimization but results in transforms that are detected as "primitives" not being animated // private static bool IsPrimitive(GameObject gameObject) // { // /* // * Primitives have the following properties: // * - have no children // * - have no non-default local transform properties // * - have MeshFilter and MeshRenderer components OR has SkinnedMeshRenderer component // */ // return gameObject.transform.childCount == 0 // && gameObject.transform.localPosition == Vector3.zero // && gameObject.transform.localRotation == Quaternion.identity // && gameObject.transform.localScale == Vector3.one // && ContainsValidRenderer(gameObject); // } public ExportFileResult ExportFile(string fileName, string mimeType, Stream stream) { if (_shouldUseInternalBufferForImages) { byte[] data = new byte[stream.Length]; stream.Read(data, 0, (int)stream.Length); stream.Close(); return new ExportFileResult { bufferView = this.ExportBufferView(data), mimeType = mimeType, }; } else { var uniqueFileName = GetUniqueName(_fileNames, fileName); _fileNames.Add(uniqueFileName); _fileInfos.Add( new FileInfo { stream = stream, uniqueFileName = uniqueFileName, } ); return new ExportFileResult { uri = uniqueFileName, }; } } private void ExportFiles(string outputPath) { for (int i = 0; i < _fileInfos.Count; ++i) { var fileInfo = _fileInfos[i]; var fileOutputPath = Path.Combine(outputPath, fileInfo.uniqueFileName); var dir = Path.GetDirectoryName(fileOutputPath); if (!Directory.Exists(dir) && dir != null) Directory.CreateDirectory(dir); var outputStream = File.Create(fileOutputPath); fileInfo.stream.Seek(0, SeekOrigin.Begin); fileInfo.stream.CopyTo(outputStream); outputStream.Close(); } } private void ExportAnimation() { for (int i = 0; i < _animatedNodes.Count; ++i) { Transform t = _animatedNodes[i]; ExportAnimationFromNode(ref t); } } #region Public API #if ANIMATION_SUPPORTED public int GetAnimationId(AnimationClip clip, Transform transform, float speed = 1) { if (_clipAndSpeedAndNodeToAnimation.TryGetValue((clip, speed, transform), out var id)) { return _root.Animations.IndexOf(id); } return -1; } #endif public MaterialId GetMaterialId(GLTFRoot root, Material materialObj) { var materialKey = 0; if (materialObj == DefaultMaterial) materialKey = 0; else if (materialObj) materialKey = materialObj.GetInstanceID(); if (_exportedMaterials.TryGetValue(materialKey, out var id)) { return new MaterialId { Id = id, Root = root }; } return null; } public TextureId GetTextureId(GLTFRoot root, Texture textureObj) { for (var i = 0; i < _textures.Count; i++) { if (_textures[i].Texture == textureObj) { return new TextureId { Id = i, Root = root }; } } return null; } public TextureId GetTextureId(GLTFRoot root, UniqueTexture textureObj) { for (var i = 0; i < _textures.Count; i++) { if (_textures[i].Equals(textureObj)) { return new TextureId { Id = i, Root = root }; } } return null; } public MeshId GetMeshId(Mesh meshObj) { foreach (var primOwner in _primOwner) { // Not sure if this is entirely accurate – we're returning the first instance here. if (primOwner.Key.Mesh == meshObj) { return primOwner.Value; } } return null; } public ImageId GetImageId(GLTFRoot root, Texture imageObj, TextureExportSettings textureMapType) { for (var i = 0; i < _imageInfos.Count; i++) { if (_imageInfos[i].texture == imageObj && _imageInfos[i].textureMapType == textureMapType) { return new ImageId { Id = i, Root = root }; } } return null; } public SamplerId GetSamplerId(GLTFRoot root, Texture textureObj) { if (_textureSettingsToSamplerIndices.TryGetValue(new SamplerRelevantTextureData(textureObj), out var samplerId)) { return new SamplerId { Id = samplerId, Root = root }; } return null; } public Texture GetTexture(int id) => _textures[id].Texture; #endregion } }