initial game submission
This commit is contained in:
1484
Packages/org.khronos.unitygltf/Runtime/Scripts/GLTFSceneExporter.cs
Normal file
1484
Packages/org.khronos.unitygltf/Runtime/Scripts/GLTFSceneExporter.cs
Normal file
@@ -0,0 +1,1484 @@
|
||||
#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);
|
||||
/// <returns>True: material export is complete. False: continue regular export.</returns>
|
||||
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<GLTFExportPluginContext> _plugins = new List<GLTFExportPluginContext>();
|
||||
|
||||
public IReadOnlyList<GLTFExportPluginContext> 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<Transform> RootTransforms => _rootTransforms;
|
||||
|
||||
private Transform[] _rootTransforms;
|
||||
private GLTFRoot _root;
|
||||
private BufferId _bufferId;
|
||||
private GLTFBuffer _buffer;
|
||||
private List<ImageInfo> _imageInfos;
|
||||
private HashSet<string> _imageExportPaths;
|
||||
private List<FileInfo> _fileInfos;
|
||||
private HashSet<string> _fileNames;
|
||||
private List<UniqueTexture> _textures;
|
||||
private Dictionary<int, int> _exportedMaterials;
|
||||
private bool _shouldUseInternalBufferForImages;
|
||||
private Dictionary<int, int> _exportedTransforms;
|
||||
private List<Transform> _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<UniqueTexture>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<UniquePrimitive, MeshId> _primOwner = new Dictionary<UniquePrimitive, MeshId>();
|
||||
|
||||
#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
|
||||
|
||||
/// <summary>
|
||||
/// Create a GLTFExporter that exports out a transform
|
||||
/// </summary>
|
||||
/// <param name="rootTransforms">Root transform of object to export</param>
|
||||
[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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a GLTFExporter that exports out a transform
|
||||
/// </summary>
|
||||
/// <param name="rootTransforms">Root transform of object to export</param>
|
||||
/// <param name="context">Export Settings</param>
|
||||
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<Transform>();
|
||||
|
||||
_exportedTransforms = new Dictionary<int, int>();
|
||||
_exportedCameras = new Dictionary<int, int>();
|
||||
_exportedLights = new Dictionary<int, int>();
|
||||
_animatedNodes = new List<Transform>();
|
||||
_skinnedNodes = new List<Transform>();
|
||||
_bakedMeshes = new Dictionary<SkinnedMeshRenderer, UnityEngine.Mesh>();
|
||||
|
||||
_root = new GLTFRoot
|
||||
{
|
||||
Accessors = new List<Accessor>(),
|
||||
Animations = new List<GLTFAnimation>(),
|
||||
Asset = new Asset
|
||||
{
|
||||
Version = "2.0",
|
||||
Generator = settings.Generator
|
||||
},
|
||||
Buffers = new List<GLTFBuffer>(),
|
||||
BufferViews = new List<BufferView>(),
|
||||
Cameras = new List<GLTFCamera>(),
|
||||
Images = new List<GLTFImage>(),
|
||||
Materials = new List<GLTFMaterial>(),
|
||||
Meshes = new List<GLTFMesh>(),
|
||||
Nodes = new List<Node>(),
|
||||
Samplers = new List<Sampler>(),
|
||||
Scenes = new List<GLTFScene>(),
|
||||
Skins = new List<Skin>(),
|
||||
Textures = new List<GLTFTexture>()
|
||||
};
|
||||
|
||||
_imageInfos = new List<ImageInfo>();
|
||||
_fileInfos = new List<FileInfo>();
|
||||
_fileNames = new HashSet<string>();
|
||||
_exportedMaterials = new Dictionary<int, int>();
|
||||
_textures = new List<UniqueTexture>();
|
||||
_imageExportPaths = new HashSet<string>();
|
||||
|
||||
_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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root object of the exported GLTF
|
||||
/// </summary>
|
||||
/// <returns>Root parsed GLTF Json</returns>
|
||||
public GLTFRoot GetRoot()
|
||||
{
|
||||
return _root;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a binary GLB file with filename at path.
|
||||
/// </summary>
|
||||
/// <param name="path">File path for saving the binary file</param>
|
||||
/// <param name="fileName">The name of the GLTF file</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory GLB creation helper. Useful for platforms where no filesystem is available (e.g. WebGL).
|
||||
/// </summary>
|
||||
/// <param name="sceneName"></param>
|
||||
/// <returns></returns>
|
||||
public byte[] SaveGLBToByteArray(string sceneName)
|
||||
{
|
||||
_shouldUseInternalBufferForImages = true;
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
SaveGLBToStream(stream, sceneName);
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a binary GLB file into a stream (memory stream, filestream, ...)
|
||||
/// </summary>
|
||||
/// <param name="path">File path for saving the binary file</param>
|
||||
/// <param name="fileName">The name of the GLTF file</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the path and filename for the GLTF Json and binary
|
||||
/// </summary>
|
||||
/// <param name="path">File path for saving the GLTF and binary files</param>
|
||||
/// <param name="fileName">The name of the GLTF file</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a specific file extension from an absolute path that may or may not already have that extension.
|
||||
/// </summary>
|
||||
/// <param name="absolutePathThatMayHaveExtension">Absolute path that may or may not already have the required extension</param>
|
||||
/// <param name="requiredExtension">The extension to ensure, with leading dot</param>
|
||||
/// <returns>An absolute path that has the required extension</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strip illegal chars and reserved words from a candidate filename (should not include the directory path)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// http://stackoverflow.com/questions/309485/c-sharp-sanitize-file-name
|
||||
/// </remarks>
|
||||
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<string>();
|
||||
}
|
||||
if(!_root.ExtensionsUsed.Contains(extension))
|
||||
{
|
||||
_root.ExtensionsUsed.Add(extension);
|
||||
}
|
||||
|
||||
if(isRequired){
|
||||
|
||||
if( _root.ExtensionsRequired == null ){
|
||||
_root.ExtensionsRequired = new List<string>();
|
||||
}
|
||||
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<Component>().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<NodeId>(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<Animation>() || nodeTransform.GetComponent<Animator>())
|
||||
{
|
||||
_animatedNodes.Add(nodeTransform);
|
||||
}
|
||||
#endif
|
||||
if (nodeTransform.GetComponent<SkinnedMeshRenderer>() && ContainsValidRenderer(nodeTransform.gameObject, settings.ExportDisabledGameObjects))
|
||||
{
|
||||
_skinnedNodes.Add(nodeTransform);
|
||||
}
|
||||
|
||||
// export camera attached to node
|
||||
Camera unityCamera = nodeTransform.GetComponent<Camera>();
|
||||
if (unityCamera != null && unityCamera.enabled)
|
||||
{
|
||||
node.Camera = ExportCamera(unityCamera);
|
||||
}
|
||||
|
||||
var lightPluginEnabled = _plugins.FirstOrDefault(x => x is LightsPunctualExportContext) != null;
|
||||
Light unityLight = nodeTransform.GetComponent<Light>();
|
||||
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<NodeId>(1);
|
||||
node.Children.Add(inbetweenId);
|
||||
|
||||
parentOfChilds = inbetween;
|
||||
}
|
||||
|
||||
parentOfChilds.Children = new List<NodeId>(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<MeshRenderer>();
|
||||
var meshFilter = gameObject.GetComponent<MeshFilter>();
|
||||
var skinnedMeshRender = gameObject.GetComponent<SkinnedMeshRenderer>();
|
||||
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<GameObject>(childCount + 1);
|
||||
var nonPrims = new List<GameObject>(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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user