// SPDX-FileCopyrightText: 2023 Unity Technologies and the glTFast authors // SPDX-License-Identifier: Apache-2.0 #if UNITY_2023_3_OR_NEWER #define ASYNC_MESH_DATA #endif using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; #if DRACO_IS_INSTALLED using Draco.Encode; #endif using GLTFast.Schema; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Profiling; using UnityEngine.Rendering; using Buffer = GLTFast.Schema.Buffer; using Camera = GLTFast.Schema.Camera; using Debug = UnityEngine.Debug; using Material = GLTFast.Schema.Material; using Mesh = GLTFast.Schema.Mesh; using Sampler = GLTFast.Schema.Sampler; using Texture = GLTFast.Schema.Texture; #if DEBUG using System.Text; #endif #if UNITY_EDITOR using UnityEditor; #endif namespace GLTFast.Export { using Logging; /// /// Provides glTF export independent of workflow (GameObjects/Entities) /// public class GltfWriter : IGltfWritable { enum State { Initialized, ContentAdded, Disposed } readonly struct AttributeData { public readonly VertexAttributeDescriptor descriptor; public readonly int inputOffset; public readonly int outputOffset; public AttributeData( VertexAttributeDescriptor descriptor, int inputOffset, int outputOffset ) { this.descriptor = descriptor; this.inputOffset = inputOffset; this.outputOffset = outputOffset; } public int Size => GetAttributeSize(descriptor.format) * descriptor.dimension; } const int k_MAXStreamCount = 4; const int k_DefaultInnerLoopBatchCount = 512; State m_State; ExportSettings m_Settings; IDeferAgent m_DeferAgent; ICodeLogger m_Logger; Root m_Gltf; HashSet m_ExtensionsUsedOnly; HashSet m_ExtensionsRequired; List m_Scenes; List m_Nodes; Dictionary m_transformToNodeId; List m_Meshes; List m_Skins; Dictionary m_MeshBindPoses; List m_SkinMesh; List m_Materials; List m_Textures; List m_Images; List m_Cameras; List m_Lights; List m_Samplers; List m_Accessors; List m_BufferViews; List m_ImageExports; List m_SamplerKeys; List m_UnityMaterials; List m_UnityMeshes; List m_MeshVertexAttributeUsage; Dictionary m_NodeMaterials; Stream m_BufferStream; string m_BufferPath; /// /// Provides glTF export independent of workflow (GameObjects/Entities) /// /// Export settings /// Defer agent (); decides when/if to preempt /// export to preserve a stable frame rate. /// Interface for logging (error) messages. public GltfWriter( ExportSettings exportSettings = null, IDeferAgent deferAgent = null, ICodeLogger logger = null ) { m_Gltf = new Root(); m_Settings = exportSettings ?? new ExportSettings(); m_Logger = logger; m_State = State.Initialized; m_DeferAgent = deferAgent ?? new UninterruptedDeferAgent(); } /// public uint AddNode( float3? translation = null, quaternion? rotation = null, float3? scale = null, uint[] children = null, string name = null ) { CertifyNotDisposed(); m_State = State.ContentAdded; var node = CreateNode(translation, rotation, scale, name); node.children = children; m_Nodes = m_Nodes ?? new List(); m_Nodes.Add(node); return (uint)m_Nodes.Count - 1; } /// [Obsolete("Use overload with skinning parameter.")] public void AddMeshToNode(int nodeId, UnityEngine.Mesh uMesh, int[] materialIds) { AddMeshToNode(nodeId, uMesh, materialIds, true); } /// [Obsolete("Use overload with skinning parameter.")] public void AddMeshToNode(int nodeId, UnityEngine.Mesh uMesh, int[] materialIds, bool skinning) { AddMeshToNode(nodeId, uMesh, materialIds, null); } /// public void AddMeshToNode( int nodeId, UnityEngine.Mesh uMesh, int[] materialIds, uint[] joints ) { if ((m_Settings.ComponentMask & ComponentType.Mesh) == 0) return; CertifyNotDisposed(); var node = m_Nodes[nodeId]; // Always export positions. var attributeUsage = VertexAttributeUsage.Position; var skinning = joints != null && joints.Length > 0; if (skinning) { attributeUsage |= VertexAttributeUsage.Skinning; } var noMaterialAssigned = false; if (materialIds != null && materialIds.Length > 0) { m_NodeMaterials ??= new Dictionary(); m_NodeMaterials[nodeId] = materialIds; foreach (var materialId in materialIds) { if (materialId < 0) { noMaterialAssigned = true; } else { attributeUsage |= GetVertexAttributeUsage(m_UnityMaterials[materialId].shader); } } } else { noMaterialAssigned = true; } if (noMaterialAssigned) { // No material. // This means the default material will be assigned, which requires positions, normals and colors. attributeUsage |= VertexAttributeUsage.Normal | VertexAttributeUsage.Color; } if (!skinning) { attributeUsage &= ~VertexAttributeUsage.Skinning; } node.mesh = AddMesh(uMesh, attributeUsage); if (skinning) { node.skin = AddSkin(node.mesh, joints); } } /// public bool AddCamera(UnityEngine.Camera uCamera, out int cameraId) { if ((m_Settings.ComponentMask & ComponentType.Camera) == 0) { cameraId = -1; return false; } CertifyNotDisposed(); var camera = new Camera(); if (uCamera.orthographic) { camera.SetCameraType(Camera.Type.Orthographic); var oSize = uCamera.orthographicSize; float aspectRatio; var targetTexture = uCamera.targetTexture; if (targetTexture == null) { aspectRatio = Screen.width / (float)Screen.height; } else { aspectRatio = targetTexture.width / (float)targetTexture.height; } camera.orthographic = new CameraOrthographic { ymag = oSize, xmag = oSize * aspectRatio, // TODO: Check if local scale should be applied to near/far znear = uCamera.nearClipPlane, zfar = uCamera.farClipPlane }; } else { camera.SetCameraType(Camera.Type.Perspective); camera.perspective = new CameraPerspective { yfov = uCamera.fieldOfView * Mathf.Deg2Rad, // TODO: Check if local scale should be applied to near/far znear = uCamera.nearClipPlane, zfar = uCamera.farClipPlane }; } if (m_Cameras == null) { m_Cameras = new List(); } cameraId = m_Cameras.Count; m_Cameras.Add(camera); return true; } /// public bool AddLight(Light uLight, out int lightId) { if ((m_Settings.ComponentMask & ComponentType.Light) == 0) { lightId = -1; return false; } CertifyNotDisposed(); var light = KhrLightsPunctual.ConvertToLight(uLight); light.intensity *= m_Settings.LightIntensityFactor; if (m_Lights == null) { m_Lights = new List(); } lightId = m_Lights.Count; m_Lights.Add(light); return true; } /// public void AddCameraToNode(int nodeId, int cameraId) { CertifyNotDisposed(); // glTF cameras face in the opposite direction, so we create a // helper node that applies the correct rotation. // TODO: Detect if this is node is already a helper node // (from glTF import) and discard it (if possible) to enable // lossless round-trips var parent = m_Nodes[nodeId]; var node = AddChildNode(nodeId, rotation: quaternion.RotateY(math.PI), name: $"{parent.name}_Orientation"); node.camera = cameraId; } /// public void AddLightToNode(int nodeId, int lightId) { CertifyNotDisposed(); var node = m_Nodes[nodeId]; var light = m_Lights[lightId]; if (light.GetLightType() != LightPunctual.Type.Point) { // glTF lights face in the opposite direction, so we create a // helper node that applies the correct rotation. // TODO: Detect if this is node is already a helper node // (from glTF import) and discard it (if possible) to enable // lossless round-trips node = AddChildNode(nodeId, rotation: quaternion.RotateY(math.PI), name: $"{node.name}_Orientation"); } node.extensions = node.extensions ?? new NodeExtensions(); node.Extensions.KHR_lights_punctual = new NodeLightsPunctual { light = lightId }; } /// public uint AddScene(uint[] nodes, string name = null) { CertifyNotDisposed(); m_Scenes = m_Scenes ?? new List(); var scene = new Scene { name = name, nodes = nodes }; m_Scenes.Add(scene); if (m_Scenes.Count == 1) { m_Gltf.scene = 0; } return (uint)m_Scenes.Count - 1; } /// public bool AddMaterial(UnityEngine.Material uMaterial, out int materialId, IMaterialExport materialExport) { if (m_Materials != null) { materialId = m_UnityMaterials.IndexOf(uMaterial); if (materialId >= 0) { return true; } } else { m_Materials = new List(); m_UnityMaterials = new List(); } var success = materialExport.ConvertMaterial(uMaterial, out var material, this, m_Logger); materialId = m_Materials.Count; m_Materials.Add(material); m_UnityMaterials.Add(uMaterial); return success; } /// public int AddImage(ImageExportBase imageExport) { #if UNITY_IMAGECONVERSION CertifyNotDisposed(); int imageId; if (m_ImageExports != null) { imageId = m_ImageExports.IndexOf(imageExport); if (imageId >= 0) { return imageId; } } else { m_ImageExports = new List(); m_Images = new List(); } imageId = m_ImageExports.Count; // TODO: Create sampler, if required // TODO: KTX encoding var image = new Image { name = imageExport.FileName, mimeType = imageExport.MimeType }; imageExport.JpgQuality = m_Settings.JpgQuality; m_ImageExports.Add(imageExport); m_Images.Add(image); return imageId; #else m_Logger?.Warning(LogCode.ImageConversionNotEnabled); return -1; #endif } /// public int AddTexture(int imageId, int samplerId) { #if UNITY_IMAGECONVERSION CertifyNotDisposed(); m_Textures = m_Textures ?? new List(); var texture = new Texture { source = imageId, sampler = samplerId }; var index = m_Textures.FindIndex(i => TextureComparer.Equals(i, texture)); if (index >= 0) { return index; } m_Textures.Add(texture); return m_Textures.Count - 1; #else return -1; #endif } /// public int AddSampler(FilterMode filterMode, TextureWrapMode wrapModeU, TextureWrapMode wrapModeV) { if (filterMode == FilterMode.Bilinear && wrapModeU == TextureWrapMode.Repeat && wrapModeV == TextureWrapMode.Repeat) { // This is the default, so no sampler needed return -1; } CertifyNotDisposed(); m_Samplers = m_Samplers ?? new List(); m_SamplerKeys = m_SamplerKeys ?? new List(); var samplerKey = new SamplerKey(filterMode, wrapModeU, wrapModeV); var index = m_SamplerKeys.IndexOf(samplerKey); if (index >= 0) { return index; } m_Samplers.Add(new Sampler(filterMode, wrapModeU, wrapModeV)); m_SamplerKeys.Add(samplerKey); return m_Samplers.Count - 1; } /// public void RegisterExtensionUsage(Extension extension, bool required = true) { CertifyNotDisposed(); if (required) { m_ExtensionsRequired = m_ExtensionsRequired ?? new HashSet(); m_ExtensionsRequired.Add(extension); } else { if (m_ExtensionsRequired == null || !m_ExtensionsRequired.Contains(extension)) { m_ExtensionsUsedOnly = m_ExtensionsUsedOnly ?? new HashSet(); m_ExtensionsUsedOnly.Add(extension); } } } /// public async Task SaveToFileAndDispose(string path) { CertifyNotDisposed(); var ext = Path.GetExtension(path); var binary = m_Settings.Format == GltfFormat.Binary; string bufferPath = null; if (!binary) { if (string.IsNullOrEmpty(ext)) { bufferPath = path + ".bin"; } else { bufferPath = path.Substring(0, path.Length - ext.Length) + ".bin"; } } var outStream = new FileStream(path, FileMode.Create); var success = await SaveAndDispose(outStream, bufferPath, Path.GetDirectoryName(path)); outStream.Close(); return success; } /// public async Task SaveToStreamAndDispose(Stream stream) { CertifyNotDisposed(); if (m_Settings.Format != GltfFormat.Binary || GetFinalImageDestination() == ImageDestination.SeparateFile) { m_Logger?.Error(LogCode.None, "Save to Stream currently only works for self-contained glTF-Binary"); return false; } return await SaveAndDispose(stream); } async Task SaveAndDispose(Stream outStream, string bufferPath = null, string directory = null) { #if DEBUG if (m_State != State.ContentAdded) { Debug.LogWarning("Exporting empty glTF"); } #endif m_BufferPath = bufferPath; var success = await Bake(Path.GetFileName(m_BufferPath), directory); if (!success) { m_BufferStream?.Close(); Dispose(); return false; } var isBinary = m_Settings.Format == GltfFormat.Binary; const uint headerSize = 12; // 4 bytes magic + 4 bytes version + 4 bytes length (uint each) const uint chunkOverhead = 8; // 4 bytes chunk length + 4 bytes chunk type (uint each) if (isBinary) { await WriteBytesToStream(outStream, BitConverter.GetBytes(GltfGlobals.GltfBinaryMagic)); await WriteBytesToStream(outStream, BitConverter.GetBytes((uint)2)); MemoryStream jsonStream = null; uint jsonLength; var outStreamCanSeek = outStream.CanSeek; if (outStreamCanSeek) { // Write empty 3 place-holder uints for: // - total length // - JSON chunk length // - JSON chunk format identifier // They'll get filled in later for (var i = 0; i < 12; i++) { outStream.WriteByte(0); } await WriteJsonToStream(outStream); jsonLength = (uint)(outStream.Length - headerSize - chunkOverhead); } else { jsonStream = new MemoryStream(); await WriteJsonToStream(jsonStream); jsonLength = (uint)jsonStream.Length; } LogSummary(jsonLength, m_BufferStream?.Length ?? 0); var jsonPad = GetPadByteCount(jsonLength); var binPad = 0; var totalLength = (uint)(headerSize + chunkOverhead + jsonLength + jsonPad); var hasBufferContent = (m_BufferStream?.Length ?? 0) > 0; if (hasBufferContent) { binPad = GetPadByteCount((uint)m_BufferStream.Length); totalLength += (uint)(chunkOverhead + m_BufferStream.Length + binPad); } if (outStreamCanSeek) { outStream.Seek(8, SeekOrigin.Begin); } await WriteBytesToStream(outStream, BitConverter.GetBytes(totalLength)); await WriteBytesToStream(outStream, BitConverter.GetBytes((uint)(jsonLength + jsonPad))); await WriteBytesToStream(outStream, BitConverter.GetBytes((uint)ChunkFormat.Json)); if (outStreamCanSeek) { outStream.Seek(0, SeekOrigin.End); } else { jsonStream.WriteTo(outStream); jsonStream.Close(); } for (var i = 0; i < jsonPad; i++) { outStream.WriteByte(0x20); } if (hasBufferContent) { await WriteBytesToStream(outStream, BitConverter.GetBytes((uint)(m_BufferStream.Length + binPad))); await WriteBytesToStream(outStream, BitConverter.GetBytes((uint)ChunkFormat.Binary)); var ms = (MemoryStream)m_BufferStream; ms.WriteTo(outStream); #if UNITY_WEBGL && !UNITY_EDITOR // FlushAsync never finishes on the Web, so doing it in sync ms.Flush(); #else await ms.FlushAsync(); #endif for (var i = 0; i < binPad; i++) { outStream.WriteByte(0); } } } else { await WriteJsonToStream(outStream); var jsonLength = 0u; if (outStream.CanSeek) { jsonLength = (uint)(outStream.Length - headerSize - chunkOverhead); } LogSummary(jsonLength, m_BufferStream?.Length ?? 0); } Dispose(); return true; } static #if UNITY_2021_3_OR_NEWER async #endif Task WriteBytesToStream(Stream outStream, byte[] bytes) { #if UNITY_2021_3_OR_NEWER await outStream.WriteAsync(bytes); #else outStream.Write(bytes); return Task.CompletedTask; #endif } async Task WriteJsonToStream(Stream outStream) { var writer = new StreamWriter(outStream); m_Gltf.GltfSerialize(writer); #if UNITY_WEBGL && !UNITY_EDITOR // FlushAsync never finishes on the Web, so doing it in sync writer.Flush(); #else await writer.FlushAsync(); #endif } void CertifyNotDisposed() { if (m_State == State.Disposed) { throw new InvalidOperationException("GltfWriter was already disposed"); } } ImageDestination GetFinalImageDestination() { var imageDest = m_Settings.ImageDestination; if (imageDest == ImageDestination.Automatic) { imageDest = m_Settings.Format == GltfFormat.Binary ? ImageDestination.MainBuffer : ImageDestination.SeparateFile; } return imageDest; } static int GetPadByteCount(uint length) { return (4 - (int)(length & 3)) & 3; } [Conditional("DEBUG")] void LogSummary(long jsonLength, long bufferLength) { #if DEBUG var sb = new StringBuilder("glTF summary: "); sb.AppendFormat("{0} bytes JSON + {1} bytes buffer", jsonLength, bufferLength); if (m_Gltf != null) { sb.AppendFormat(", {0} nodes", m_Gltf.Nodes?.Count ?? 0); sb.AppendFormat(" ,{0} meshes", m_Gltf.meshes?.Length ?? 0); sb.AppendFormat(" ,{0} materials", m_Gltf.Materials?.Count ?? 0); sb.AppendFormat(" ,{0} images", m_Gltf.Images?.Count ?? 0); } m_Logger?.Info(sb.ToString()); #endif } async Task Bake(string bufferPath, string directory) { var success = true; if (m_Meshes != null && m_Meshes.Count > 0) { success = await BakeMeshes(); if (!success) return false; } AssignBindPosesToSkins(); AssignMaterialsToMeshes(); success = await BakeImages(directory); if (!success) return false; if (m_BufferStream != null && m_BufferStream.Length > 0) { m_Gltf.buffers = new[] { new Buffer { uri = bufferPath, byteLength = (uint) m_BufferStream.Length } }; } m_Gltf.scenes = m_Scenes?.ToArray(); m_Gltf.nodes = m_Nodes?.ToArray(); m_Gltf.meshes = m_Meshes?.ToArray(); m_Gltf.skins = m_Skins?.ToArray(); m_Gltf.accessors = m_Accessors?.ToArray(); m_Gltf.bufferViews = m_BufferViews?.ToArray(); m_Gltf.materials = m_Materials?.ToArray(); m_Gltf.images = m_Images?.ToArray(); m_Gltf.textures = m_Textures?.ToArray(); m_Gltf.samplers = m_Samplers?.ToArray(); m_Gltf.cameras = m_Cameras?.ToArray(); if (m_Lights != null && m_Lights.Count > 0) { RegisterExtensionUsage(Extension.LightsPunctual); m_Gltf.extensions = m_Gltf.extensions ?? new Schema.RootExtensions(); m_Gltf.extensions.KHR_lights_punctual = m_Gltf.extensions.KHR_lights_punctual ?? new LightsPunctual(); m_Gltf.extensions.KHR_lights_punctual.lights = m_Lights.ToArray(); } m_Gltf.asset = new Asset { version = "2.0", generator = $"Unity {Application.unityVersion} glTFast {Constants.version}" }; BakeExtensions(); return true; } void BakeExtensions() { if (m_ExtensionsRequired != null) { var usedOnlyCount = m_ExtensionsUsedOnly?.Count ?? 0; m_Gltf.extensionsRequired = new string[m_ExtensionsRequired.Count]; m_Gltf.extensionsUsed = new string[m_ExtensionsRequired.Count + usedOnlyCount]; var i = 0; foreach (var extension in m_ExtensionsRequired) { var name = extension.GetName(); Assert.IsFalse(string.IsNullOrEmpty(name)); m_Gltf.extensionsRequired[i] = name; m_Gltf.extensionsUsed[i] = name; i++; } } if (m_ExtensionsUsedOnly != null) { var i = 0; if (m_Gltf.extensionsUsed == null) { m_Gltf.extensionsUsed = new string[m_ExtensionsUsedOnly.Count]; } else { i = m_Gltf.extensionsUsed.Length - m_ExtensionsUsedOnly.Count; } foreach (var extension in m_ExtensionsUsedOnly) { m_Gltf.extensionsUsed[i++] = extension.GetName(); } } } void AssignBindPosesToSkins() { if (m_SkinMesh == null || m_MeshBindPoses == null) return; for (var skinId = 0; skinId < m_SkinMesh.Count; skinId++) { var meshId = m_SkinMesh[skinId]; var inverseBindMatricesAccessor = m_MeshBindPoses[meshId]; m_Skins[skinId].inverseBindMatrices = inverseBindMatricesAccessor; } m_SkinMesh = null; m_MeshBindPoses = null; } void AssignMaterialsToMeshes() { if (m_NodeMaterials != null && m_Meshes != null) { var meshMaterialCombos = new Dictionary(m_Meshes.Count); var originalCombos = new Dictionary(m_Meshes.Count); foreach (var nodeMaterial in m_NodeMaterials) { var nodeId = nodeMaterial.Key; var materialIds = nodeMaterial.Value; var node = m_Nodes[nodeId]; var originalMeshId = node.mesh; if (originalMeshId < 0) continue; var mesh = m_Meshes[originalMeshId]; var meshMaterialCombo = new MeshMaterialCombination(originalMeshId, materialIds); if (!originalCombos.ContainsKey(originalMeshId)) { // First usage of the original -> assign materials to original AssignMaterialsToMesh(materialIds, mesh); originalCombos[originalMeshId] = meshMaterialCombo; meshMaterialCombos[meshMaterialCombo] = originalMeshId; } else { // Mesh is re-used -> check if this exact materials set was used before if (meshMaterialCombos.TryGetValue(meshMaterialCombo, out var meshId)) { // Materials are identical -> re-use Mesh object node.mesh = meshId; } else { // Materials differ -> clone Mesh object and assign materials to clone var clonedMeshId = DuplicateMesh(originalMeshId); mesh = m_Meshes[clonedMeshId]; AssignMaterialsToMesh(materialIds, mesh); node.mesh = clonedMeshId; meshMaterialCombos[meshMaterialCombo] = clonedMeshId; } } } } m_NodeMaterials = null; } static void AssignMaterialsToMesh(int[] materialIds, Mesh mesh) { for (var i = 0; i < materialIds.Length && i < mesh.primitives.Length; i++) { mesh.primitives[i].material = materialIds[i] >= 0 ? materialIds[i] : -1; } } int DuplicateMesh(int meshId) { var src = m_Meshes[meshId]; var copy = (Mesh)src.Clone(); m_Meshes.Add(copy); return m_Meshes.Count - 1; } async Task BakeMeshes() { Profiler.BeginSample("AcquireReadOnlyMeshData"); if ((m_Settings.Compression & Compression.Draco) != 0) { #if DRACO_IS_INSTALLED RegisterExtensionUsage(Extension.DracoMeshCompression); if (m_Settings.DracoSettings == null) { //Ensure fallback to default settings m_Settings.DracoSettings = new DracoExportSettings(); } if ((m_Settings.Compression & Compression.Uncompressed) != 0) { m_Logger?.Warning(LogCode.UncompressedFallbackNotSupported); } #else m_Logger?.Error(LogCode.PackageMissing, "Draco For Unity", ExtensionName.DracoMeshCompression); return false; #endif } else if ((m_Settings.Compression & Compression.MeshOpt) != 0) { m_Logger?.Error("Meshopt compression is not supported yet."); return false; } var tasks = m_Settings.Deterministic ? null : new List(m_Meshes.Count); var meshData = CollectMeshData(out var meshDataArray); Profiler.EndSample(); for (var meshId = 0; meshId < m_Meshes.Count; meshId++) { Task task; #if DRACO_IS_INSTALLED if ((m_Settings.Compression & Compression.Draco) != 0) { task = BakeMeshDraco(meshId); } else #endif { task = BakeMesh(meshId, meshData[meshId]); } if (m_Settings.Deterministic || tasks == null) { await task; } else { tasks.Add(task); } await m_DeferAgent.BreakPoint(); } if (!m_Settings.Deterministic) { await Task.WhenAll(tasks); } meshDataArray?.Dispose(); return true; } IMeshData[] CollectMeshData(out UnityEngine.Mesh.MeshDataArray? meshDataArray) { var meshData = new IMeshData[m_UnityMeshes.Count]; var nonReadableMesh = false; var readableMeshCount = 0; List readableMeshes = null; List indexMap = null; for (var i = 0; i < m_UnityMeshes.Count; i++) { var mesh = m_UnityMeshes[i]; if (mesh.isReadable) { if (nonReadableMesh) { // There's been a non-readable mesh before, so put this mesh in the queue. if (readableMeshes == null) { readableMeshes = new List(); indexMap = new List(); } readableMeshes.Add(mesh); indexMap.Add(i); } readableMeshCount++; } else { #if UNITY_2021_3_OR_NEWER meshData[i] = mesh.indexFormat == IndexFormat.UInt16 ? new NonReadableMeshData(mesh) : new NonReadableMeshData(mesh); if (readableMeshes == null && readableMeshCount > 0) { // This is the first non-readable mesh, so all potential predecessors are readable and we put // them in the queue. readableMeshes = new List(i); indexMap = new List(i); for (var a = 0; a < i; a++) { readableMeshes.Add(m_UnityMeshes[a]); indexMap.Add(a); } } #endif nonReadableMesh = true; } } meshDataArray = null; if (readableMeshCount > 0) { if (readableMeshes == null) { // All meshes are readable, bulk acquire data for all of them. meshDataArray = UnityEngine.Mesh.AcquireReadOnlyMeshData(m_UnityMeshes); for (var i = 0; i < m_UnityMeshes.Count; i++) { meshData[i] = m_UnityMeshes[i].indexFormat == IndexFormat.UInt16 ? (IMeshData)new MeshDataProxy(meshDataArray.Value[i]) : new MeshDataProxy(meshDataArray.Value[i]); } } else { // Only a subset of the meshes are readable. meshDataArray = UnityEngine.Mesh.AcquireReadOnlyMeshData(readableMeshes); for (var i = 0; i < readableMeshes.Count; i++) { var actualIndex = indexMap[i]; meshData[actualIndex] = m_UnityMeshes[actualIndex].indexFormat == IndexFormat.UInt16 ? (IMeshData)new MeshDataProxy(meshDataArray.Value[i]) : new MeshDataProxy(meshDataArray.Value[i]); } } } return meshData; } async Task BakeMesh(int meshId, IMeshData meshData) { Profiler.BeginSample("BakeMesh 1"); var mesh = m_Meshes[meshId]; var uMesh = m_UnityMeshes[meshId]; var vertexAttributeUsage = m_Settings.PreservedVertexAttributes | m_MeshVertexAttributeUsage[meshId]; var vertexAttributes = uMesh.GetVertexAttributes(); var inputStrides = new int[k_MAXStreamCount]; var outputStrides = new int[k_MAXStreamCount]; var alignments = new int[k_MAXStreamCount]; var streamAccessorIds = new List[k_MAXStreamCount]; var attributes = new Attributes(); var vertexCount = uMesh.vertexCount; var attrDataDict = new Dictionary(); foreach (var attribute in vertexAttributes) { var excludeAttribute = (attribute.attribute.ToVertexAttributeUsage() & vertexAttributeUsage) == VertexAttributeUsage.None; var attributeElementSize = GetAttributeSize(attribute.format); var attributeSize = attribute.dimension * attributeElementSize; var attrData = new AttributeData( attribute, inputStrides[attribute.stream], outputStrides[attribute.stream] ); inputStrides[attribute.stream] += attributeSize; alignments[attribute.stream] = math.max(alignments[attribute.stream], attributeElementSize); if (excludeAttribute) { continue; } outputStrides[attribute.stream] += attributeSize; // Adhere data alignment rules Assert.IsTrue(attrData.outputOffset % 4 == 0); var accessor = new Accessor { byteOffset = attrData.outputOffset, componentType = Accessor.GetComponentType(attribute.format), count = vertexCount, }; accessor.SetAttributeType(Accessor.GetAccessorAttributeType(attribute.dimension)); var accessorId = AddAccessor(accessor); streamAccessorIds[attribute.stream] ??= new List(); streamAccessorIds[attribute.stream].Add(accessorId); attrDataDict[attribute.attribute] = attrData; switch (attribute.attribute) { case VertexAttribute.Position: var bounds = uMesh.bounds; var max = bounds.max; var min = bounds.min; accessor.min = new[] { -max.x, min.y, min.z }; accessor.max = new[] { -min.x, max.y, max.z }; attributes.POSITION = accessorId; break; case VertexAttribute.Normal: attributes.NORMAL = accessorId; break; case VertexAttribute.Tangent: Assert.AreEqual(4, attribute.dimension, "Invalid tangent vector dimension"); attributes.TANGENT = accessorId; break; case VertexAttribute.Color: attributes.COLOR_0 = accessorId; break; case VertexAttribute.TexCoord0: attributes.TEXCOORD_0 = accessorId; break; case VertexAttribute.TexCoord1: attributes.TEXCOORD_1 = accessorId; break; case VertexAttribute.TexCoord2: attributes.TEXCOORD_2 = accessorId; break; case VertexAttribute.TexCoord3: attributes.TEXCOORD_3 = accessorId; break; case VertexAttribute.TexCoord4: attributes.TEXCOORD_4 = accessorId; break; case VertexAttribute.TexCoord5: attributes.TEXCOORD_5 = accessorId; break; case VertexAttribute.TexCoord6: attributes.TEXCOORD_6 = accessorId; break; case VertexAttribute.TexCoord7: attributes.TEXCOORD_7 = accessorId; break; case VertexAttribute.BlendWeight: attributes.WEIGHTS_0 = accessorId; break; case VertexAttribute.BlendIndices: attributes.JOINTS_0 = accessorId; accessor.componentType = GltfComponentType.UnsignedShort; break; default: throw new ArgumentOutOfRangeException(); } } await ExportBindPoses(meshId, uMesh); var streamCount = 1; for (var stream = 0; stream < outputStrides.Length; stream++) { var stride = outputStrides[stream]; if (stride <= 0) continue; streamCount = stream + 1; } var indexComponentType = uMesh.indexFormat == IndexFormat.UInt16 ? GltfComponentType.UnsignedShort : GltfComponentType.UnsignedInt; mesh.primitives = new MeshPrimitive[meshData.subMeshCount]; var indexAccessors = new Accessor[meshData.subMeshCount]; var indexOffset = 0; MeshTopology? topology = null; for (var subMeshIndex = 0; subMeshIndex < meshData.subMeshCount; subMeshIndex++) { var subMeshTopology = meshData.GetTopology(subMeshIndex); if (!topology.HasValue) { topology = subMeshTopology; } else if (topology.Value != subMeshTopology) { m_Logger?.Error(LogCode.TopologyUnsupported, "mixed"); return; } var mode = GetDrawMode(subMeshTopology); if (!mode.HasValue) { m_Logger?.Error(LogCode.TopologyUnsupported, subMeshTopology.ToString()); mode = DrawMode.Points; } var indexAccessor = new Accessor { byteOffset = indexOffset, componentType = indexComponentType, count = meshData.GetIndexCount(subMeshIndex), // min = new []{}, // TODO // max = new []{}, // TODO }; indexAccessor.SetAttributeType(GltfAccessorAttributeType.SCALAR); if (subMeshTopology == MeshTopology.Quads) { indexAccessor.count = indexAccessor.count / 2 * 3; } var indexAccessorId = AddAccessor(indexAccessor); indexAccessors[subMeshIndex] = indexAccessor; indexOffset += indexAccessor.count * Accessor.GetComponentTypeSize(indexComponentType); mesh.primitives[subMeshIndex] = new MeshPrimitive { mode = mode.Value, attributes = attributes, indices = indexAccessorId, }; } Profiler.EndSample(); // "BakeMesh 1" if (!topology.HasValue) { m_Logger?.Error(LogCode.TopologyUnsupported, "unknown"); return; } var indexBufferViewId = await BakeMeshIndices(meshData, uMesh, topology); foreach (var accessor in indexAccessors) { accessor.bufferView = indexBufferViewId; } var inputStreams = new NativeArray[streamCount]; var outputStreams = new NativeArray[streamCount]; for (var stream = 0; stream < streamCount; stream++) { inputStreams[stream] = #if ASYNC_MESH_DATA await #endif meshData.GetVertexData(stream); outputStreams[stream] = new NativeArray(outputStrides[stream] * vertexCount, Allocator.Persistent); } foreach (var pair in attrDataDict) { var vertexAttribute = pair.Key; var attrData = pair.Value; switch (vertexAttribute) { case VertexAttribute.Position: case VertexAttribute.Normal: await ConvertPositionAttribute( attrData, (uint)inputStrides[attrData.descriptor.stream], (uint)outputStrides[attrData.descriptor.stream], vertexCount, inputStreams[attrData.descriptor.stream], outputStreams[attrData.descriptor.stream] ); break; case VertexAttribute.Tangent: await ConvertTangentAttribute( attrData, (uint)inputStrides[attrData.descriptor.stream], (uint)outputStrides[attrData.descriptor.stream], vertexCount, inputStreams[attrData.descriptor.stream], outputStreams[attrData.descriptor.stream] ); break; case VertexAttribute.TexCoord0: case VertexAttribute.TexCoord1: case VertexAttribute.TexCoord2: case VertexAttribute.TexCoord3: case VertexAttribute.TexCoord4: case VertexAttribute.TexCoord5: case VertexAttribute.TexCoord6: case VertexAttribute.TexCoord7: await ConvertTexCoordAttribute( attrData, (uint)inputStrides[attrData.descriptor.stream], (uint)outputStrides[attrData.descriptor.stream], vertexCount, inputStreams[attrData.descriptor.stream], outputStreams[attrData.descriptor.stream] ); break; case VertexAttribute.Color: case VertexAttribute.BlendWeight: await ConvertSkinWeightsAttribute( attrData, (uint)inputStrides[attrData.descriptor.stream], (uint)outputStrides[attrData.descriptor.stream], vertexCount, inputStreams[attrData.descriptor.stream], outputStreams[attrData.descriptor.stream] ); break; case VertexAttribute.BlendIndices: Profiler.BeginSample("ConvertSkinningAttributesJob"); // indices are uint*4 in Unity, and ushort*4 in glTF await ConvertSkinIndicesAttributes( attrData, (uint)inputStrides[attrData.descriptor.stream], (uint)outputStrides[attrData.descriptor.stream], vertexCount, inputStreams[attrData.descriptor.stream], outputStreams[attrData.descriptor.stream] ); Profiler.EndSample(); break; default: await ConvertGenericAttribute( attrData, (uint)inputStrides[attrData.descriptor.stream], (uint)outputStrides[attrData.descriptor.stream], vertexCount, inputStreams[attrData.descriptor.stream], outputStreams[attrData.descriptor.stream] ); break; } } var bufferViewIds = new int[streamCount]; for (var stream = 0; stream < streamCount; stream++) { var bufferViewId = WriteBufferViewToBuffer( outputStreams[stream], BufferViewTarget.ArrayBuffer, outputStrides[stream], alignments[stream] ); bufferViewIds[stream] = bufferViewId; inputStreams[stream].Dispose(); outputStreams[stream].Dispose(); var accessorIds = streamAccessorIds[stream]; if (accessorIds != null) { foreach (var accessorId in accessorIds) { m_Accessors[accessorId].bufferView = bufferViewId; } } } } async Task BakeMeshIndices(IMeshData meshData, UnityEngine.Mesh uMesh, MeshTopology? topology) { NativeArray indices; if (uMesh.indexFormat == IndexFormat.UInt16) { using var indexData16 = #if ASYNC_MESH_DATA await #endif ((IMeshData)meshData).GetIndexData(); NativeArray destIndices; JobHandle job = default; if (topology.Value == MeshTopology.Quads) { Profiler.BeginSample("IndexJobUInt16QuadsSchedule"); var quadCount = indexData16.Length / 4; destIndices = new NativeArray(quadCount * 6, Allocator.TempJob); JobHandle ConvertSubmeshIndices(int submeshIndex, JobHandle dependency) { var submesh = uMesh.GetSubMesh(submeshIndex); Assert.AreEqual(0, submesh.indexStart % 4); Assert.AreEqual(0, submesh.indexCount % 4); var dstStart = submesh.indexStart / 4 * 6; var dstLength = submesh.indexCount / 4 * 6; job = new ExportJobs.ConvertIndicesQuadFlippedJobUInt16 { input = indexData16.GetSubArray(submesh.indexStart, submesh.indexCount), result = destIndices.GetSubArray(dstStart, dstLength), baseVertexOffset = (ushort)submesh.baseVertex }.Schedule(submesh.indexCount / 4, k_DefaultInnerLoopBatchCount, dependency); return job; } job = ConvertSubmeshIndices(0, job); for (var i = 1; i < uMesh.subMeshCount; i++) { job = ConvertSubmeshIndices(i, job); } Profiler.EndSample(); } else { Profiler.BeginSample("IndexJobUInt16TrisSchedule"); destIndices = new NativeArray(indexData16.Length, Allocator.TempJob); JobHandle ConvertSubmeshIndices(int submeshIndex, JobHandle dependency) { var submesh = uMesh.GetSubMesh(submeshIndex); Assert.AreEqual(0, submesh.indexStart % 3); Assert.AreEqual(0, submesh.indexCount % 3); job = new ExportJobs.ConvertIndicesFlippedJobUInt16 { input = indexData16.GetSubArray(submesh.indexStart, submesh.indexCount), result = destIndices.GetSubArray(submesh.indexStart, submesh.indexCount), baseVertexOffset = (ushort)submesh.baseVertex }.Schedule(submesh.indexCount / 3, k_DefaultInnerLoopBatchCount, dependency); return job; } job = ConvertSubmeshIndices(0, job); for (var i = 1; i < uMesh.subMeshCount; i++) { job = ConvertSubmeshIndices(i, job); } Profiler.EndSample(); } while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); indices = destIndices.Reinterpret(sizeof(ushort)); } else { using var indexData32 = #if ASYNC_MESH_DATA await #endif ((IMeshData)meshData).GetIndexData(); NativeArray destIndices; JobHandle job = default; if (topology.Value == MeshTopology.Quads) { Profiler.BeginSample("IndexJobUInt32QuadsSchedule"); var quadCount = indexData32.Length / 4; destIndices = new NativeArray(quadCount * 6, Allocator.TempJob); JobHandle ConvertSubmeshIndices(int submeshIndex, JobHandle dependency) { var submesh = uMesh.GetSubMesh(submeshIndex); Assert.AreEqual(0, submesh.indexStart % 4); Assert.AreEqual(0, submesh.indexCount % 4); var dstStart = submesh.indexStart / 4 * 6; var dstLength = submesh.indexCount / 4 * 6; job = new ExportJobs.ConvertIndicesQuadFlippedJobUInt32 { input = indexData32.GetSubArray(submesh.indexStart, submesh.indexCount), result = destIndices.GetSubArray(dstStart, dstLength), baseVertexOffset = (ushort)submesh.baseVertex }.Schedule(submesh.indexCount / 4, k_DefaultInnerLoopBatchCount, dependency); return job; } job = ConvertSubmeshIndices(0, job); for (var i = 1; i < uMesh.subMeshCount; i++) { job = ConvertSubmeshIndices(i, job); } Profiler.EndSample(); } else { Profiler.BeginSample("IndexJobUInt32TrisSchedule"); destIndices = new NativeArray(indexData32.Length, Allocator.TempJob); JobHandle ConvertSubmeshIndices(int submeshIndex, JobHandle dependency) { var submesh = uMesh.GetSubMesh(submeshIndex); Assert.AreEqual(0, submesh.indexStart % 3); Assert.AreEqual(0, submesh.indexCount % 3); job = new ExportJobs.ConvertIndicesFlippedJobUInt32 { input = indexData32.GetSubArray(submesh.indexStart, submesh.indexCount), result = destIndices.GetSubArray(submesh.indexStart, submesh.indexCount), baseVertexOffset = (uint)submesh.baseVertex }.Schedule(submesh.indexCount / 3, k_DefaultInnerLoopBatchCount, dependency); return job; } job = ConvertSubmeshIndices(0, job); for (var i = 1; i < uMesh.subMeshCount; i++) { job = ConvertSubmeshIndices(i, job); } Profiler.EndSample(); } while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); indices = destIndices.Reinterpret(sizeof(uint)); } Profiler.BeginSample("IndexJobPostWork"); var indexBufferViewId = WriteBufferViewToBuffer( indices, BufferViewTarget.ElementArrayBuffer, byteAlignment: sizeof(ushort) ); indices.Dispose(); Profiler.EndSample(); return indexBufferViewId; } async Task ExportBindPoses(int meshId, UnityEngine.Mesh uMesh) { // Add skin var bindposes = uMesh.bindposes; if (bindposes != null && bindposes.Length > 0) { var accessor = new Accessor { byteOffset = 0, componentType = GltfComponentType.Float, count = bindposes.Length }; accessor.SetAttributeType(GltfAccessorAttributeType.MAT4); var accessorId = AddAccessor(accessor); m_MeshBindPoses ??= new Dictionary(); m_MeshBindPoses[meshId] = accessorId; var bufferViewId = await WriteBindPosesToBuffer(bindposes); accessor.bufferView = bufferViewId; } } async Task WriteBindPosesToBuffer(Matrix4x4[] bindposes) { var bufferViewId = -1; #pragma warning disable CS0618 // Type or member is obsolete // See original ObsoleteAttribute: // > ManagedNativeArray is going to get sealed or removed from the public API in a future release. var nativeBindPoses = new ManagedNativeArray(bindposes); #pragma warning restore CS0618 // Type or member is obsolete var matrices = nativeBindPoses.nativeArray; var job = new ExportJobs.ConvertMatrixJob { matrices = matrices }.Schedule(bindposes.Length, k_DefaultInnerLoopBatchCount); while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); bufferViewId = WriteBufferViewToBuffer( matrices.Reinterpret(sizeof(float) * 4 * 4), BufferViewTarget.None, byteAlignment: 4 ); nativeBindPoses.Dispose(); return bufferViewId; } #if DRACO_IS_INSTALLED async Task BakeMeshDraco(int meshId) { var mesh = m_Meshes[meshId]; var unityMesh = m_UnityMeshes[meshId]; #if UNITY_EDITOR // Non-readable meshes are unsupported during playmode or in builds, but work in Editor exports. if (Application.isPlaying) #endif { if (!unityMesh.isReadable) { return; } } var results = await DracoEncoder.EncodeMesh( unityMesh, (QuantizationSettings) m_Settings.DracoSettings, (SpeedSettings) m_Settings.DracoSettings ); if (results == null) return; mesh.primitives = new MeshPrimitive[results.Length]; for (var submesh = 0; submesh < results.Length; submesh++) { var encodeResult = results[submesh]; var bufferViewId = WriteBufferViewToBuffer(encodeResult.data, BufferViewTarget.None); var attributes = new Attributes(); var dracoAttributes = new Attributes(); foreach ( var vertexAttributeTuple in encodeResult.vertexAttributes) { var vertexAttribute = vertexAttributeTuple.Key; var attribute = vertexAttributeTuple.Value; var accessor = new Accessor { componentType = vertexAttribute == VertexAttribute.BlendIndices ? GltfComponentType.UnsignedShort : GltfComponentType.Float, count = (int)encodeResult.vertexCount }; var attributeType = Accessor.GetAccessorAttributeType(attribute.dimensions); accessor.SetAttributeType(attributeType); var accessorId = AddAccessor(accessor); if (vertexAttribute == VertexAttribute.Position) { var submeshDesc = unityMesh.GetSubMesh(submesh); var bounds = submeshDesc.bounds; var center = bounds.center; var extents = bounds.extents; accessor.min = new[] { -center.x-extents.x, center.y-extents.y, center.z-extents.z }; accessor.max = new[] { -center.x+extents.x, center.y+extents.y, center.z+extents.z }; } SetAttributesByType( vertexAttribute, attributes, dracoAttributes, accessorId, (int)attribute.identifier ); } var indexAccessor = new Accessor { componentType = GltfComponentType.UnsignedInt, count = (int)encodeResult.indexCount }; indexAccessor.SetAttributeType(GltfAccessorAttributeType.SCALAR); var indicesId = AddAccessor(indexAccessor); mesh.primitives[submesh] = new MeshPrimitive { extensions = new MeshPrimitiveExtensions { KHR_draco_mesh_compression = new MeshPrimitiveDracoExtension { bufferView = bufferViewId, attributes = dracoAttributes } }, attributes = attributes, indices = indicesId }; } await ExportBindPoses(meshId, unityMesh); } static void SetAttributesByType( VertexAttribute type, Attributes attributes, Attributes dracoAttributes, int accessorId, int dracoId ) { switch (type) { case VertexAttribute.Position: attributes.POSITION = accessorId; dracoAttributes.POSITION = dracoId; break; case VertexAttribute.Normal: attributes.NORMAL = accessorId; dracoAttributes.NORMAL = dracoId; break; case VertexAttribute.Tangent: attributes.TANGENT = accessorId; dracoAttributes.TANGENT = dracoId; break; case VertexAttribute.Color: attributes.COLOR_0 = accessorId; dracoAttributes.COLOR_0 = dracoId; break; case VertexAttribute.TexCoord0: attributes.TEXCOORD_0 = accessorId; dracoAttributes.TEXCOORD_0 = dracoId; break; case VertexAttribute.TexCoord1: attributes.TEXCOORD_1 = accessorId; dracoAttributes.TEXCOORD_1 = dracoId; break; case VertexAttribute.TexCoord2: attributes.TEXCOORD_2 = accessorId; dracoAttributes.TEXCOORD_2 = dracoId; break; case VertexAttribute.TexCoord3: attributes.TEXCOORD_3 = accessorId; dracoAttributes.TEXCOORD_3 = dracoId; break; case VertexAttribute.TexCoord4: attributes.TEXCOORD_4 = accessorId; dracoAttributes.TEXCOORD_4 = dracoId; break; case VertexAttribute.TexCoord5: attributes.TEXCOORD_5 = accessorId; dracoAttributes.TEXCOORD_5 = dracoId; break; case VertexAttribute.TexCoord6: attributes.TEXCOORD_6 = accessorId; dracoAttributes.TEXCOORD_6 = dracoId; break; case VertexAttribute.TexCoord7: attributes.TEXCOORD_7 = accessorId; dracoAttributes.TEXCOORD_7 = dracoId; break; case VertexAttribute.BlendWeight: attributes.WEIGHTS_0 = accessorId; dracoAttributes.WEIGHTS_0 = dracoId; break; case VertexAttribute.BlendIndices: attributes.JOINTS_0 = accessorId; dracoAttributes.JOINTS_0 = dracoId; break; } } #endif // DRACO_IS_INSTALLED int AddAccessor(Accessor accessor) { m_Accessors = m_Accessors ?? new List(); var accessorId = m_Accessors.Count; m_Accessors.Add(accessor); return accessorId; } async Task BakeImages(string directory) { if (m_ImageExports != null) { Dictionary fileNameOverrides = null; var imageDest = GetFinalImageDestination(); var overwrite = m_Settings.FileConflictResolution == FileConflictResolution.Overwrite; if (!overwrite && imageDest == ImageDestination.SeparateFile) { var fileExists = false; var fileNames = new HashSet( #if NET_STANDARD m_ImageExports.Count #endif ); bool GetUniqueFileName(ref string filename) { if (fileNames.Contains(filename)) { var i = 0; var extension = Path.GetExtension(filename); var baseName = Path.GetFileNameWithoutExtension(filename); string newName; do { newName = $"{baseName}_{i++}{extension}"; } while (fileNames.Contains(newName)); filename = newName; return true; } return false; } for (var imageId = 0; imageId < m_ImageExports.Count; imageId++) { var imageExport = m_ImageExports[imageId]; var fileName = Path.GetFileName(imageExport.FileName); if (GetUniqueFileName(ref fileName)) { fileNameOverrides = fileNameOverrides ?? new Dictionary(); fileNameOverrides[imageId] = fileName; } fileNames.Add(fileName); var destPath = Path.Combine(directory, fileName); if (File.Exists(destPath)) { fileExists = true; } } if (fileExists) { #if UNITY_EDITOR overwrite = EditorUtility.DisplayDialog( "Image file conflicts", "Some image files at the destination will be overwritten", "Overwrite", "Cancel"); if (!overwrite) { return false; } #else if (m_Settings.FileConflictResolution == FileConflictResolution.Abort) { return false; } #endif } } for (var imageId = 0; imageId < m_ImageExports.Count; imageId++) { var imageExport = m_ImageExports[imageId]; if (imageDest == ImageDestination.MainBuffer) { // TODO: Write from file to buffer stream directly var imageBytes = imageExport.GetData(); if (imageBytes != null) { m_Images[imageId].bufferView = WriteBufferViewToBuffer(imageBytes, BufferViewTarget.None); } } else if (imageDest == ImageDestination.SeparateFile) { if (!(fileNameOverrides != null && fileNameOverrides.TryGetValue(imageId, out var fileName))) { fileName = imageExport.FileName; } if (imageExport.Write(Path.Combine(directory, fileName), overwrite)) { m_Images[imageId].uri = fileName; } else { m_Images[imageId] = null; } } await m_DeferAgent.BreakPoint(); } } m_ImageExports = null; return true; } static async Task ConvertSkinWeightsAttribute( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { var job = ConvertSkinWeightsAttributeJob(attrData, inputByteStride, outputByteStride, vertexCount, inputStream, outputStream); while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); // TODO: Wait until thread is finished } static async Task ConvertPositionAttribute( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { var job = CreateConvertPositionAttributeJob( attrData, inputByteStride, outputByteStride, vertexCount, inputStream, outputStream ); while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); } static unsafe JobHandle CreateConvertPositionAttributeJob( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { if (attrData.descriptor.format == VertexAttributeFormat.Float16) { return new ExportJobs.ConvertPositionHalfJob { input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.inputOffset, inputByteStride = inputByteStride, outputByteStride = outputByteStride, output = (byte*)outputStream.GetUnsafePtr() + attrData.outputOffset }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); } Assert.AreEqual(VertexAttributeFormat.Float32, attrData.descriptor.format, "Unsupported positions/normals format"); return new ExportJobs.ConvertPositionFloatJob { input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.inputOffset, inputByteStride = inputByteStride, outputByteStride = outputByteStride, output = (byte*)outputStream.GetUnsafePtr() + attrData.outputOffset }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); } static async Task ConvertTangentAttribute( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { var job = CreateConvertTangentAttributeJob( attrData, inputByteStride, outputByteStride, vertexCount, inputStream, outputStream ); while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); } static unsafe JobHandle CreateConvertTangentAttributeJob( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { if (attrData.descriptor.format == VertexAttributeFormat.Float16) { return new ExportJobs.ConvertTangentHalfJob { input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.inputOffset, inputByteStride = inputByteStride, outputByteStride = outputByteStride, output = (byte*)outputStream.GetUnsafePtr() + attrData.outputOffset }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); } Assert.AreEqual(VertexAttributeFormat.Float32, attrData.descriptor.format, "Unsupported tangents format"); return new ExportJobs.ConvertTangentFloatJob { input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.inputOffset, inputByteStride = inputByteStride, outputByteStride = outputByteStride, output = (byte*)outputStream.GetUnsafePtr() + attrData.outputOffset }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); } static async Task ConvertTexCoordAttribute( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { var job = CreateConvertTexCoordAttributeJob( attrData, inputByteStride, outputByteStride, vertexCount, inputStream, outputStream); while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); } static async Task ConvertGenericAttribute( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { var job = CreateConvertGenericAttributeJob( attrData, inputByteStride, outputByteStride, vertexCount, inputStream, outputStream); while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); } static unsafe JobHandle CreateConvertTexCoordAttributeJob( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { if (attrData.descriptor.format == VertexAttributeFormat.Float16) { return new ExportJobs.ConvertTexCoordHalfJob { input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.inputOffset, inputByteStride = inputByteStride, outputByteStride = outputByteStride, output = (byte*)outputStream.GetUnsafePtr() + attrData.outputOffset }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); } return new ExportJobs.ConvertTexCoordFloatJob { input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.inputOffset, inputByteStride = inputByteStride, outputByteStride = outputByteStride, output = (byte*)outputStream.GetUnsafePtr() + attrData.outputOffset }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); } static unsafe JobHandle ConvertSkinWeightsAttributeJob( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { var job = new ExportJobs.ConvertSkinWeightsJob { input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.inputOffset, inputByteStride = inputByteStride, outputByteStride = outputByteStride, output = (byte*)outputStream.GetUnsafePtr() + attrData.outputOffset, }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); return job; } static async Task ConvertSkinIndicesAttributes( AttributeData indicesAttrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray input, NativeArray output ) { var job = CreateConvertSkinIndicesAttributesJob(indicesAttrData, inputByteStride, outputByteStride, vertexCount, input, output); while (!job.IsCompleted) { await Task.Yield(); } job.Complete(); } static unsafe JobHandle CreateConvertSkinIndicesAttributesJob( AttributeData indicesAttrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray input, NativeArray output ) { var job = new ExportJobs.ConvertSkinIndicesJob { input = (byte*)input.GetUnsafeReadOnlyPtr(), inputByteStride = inputByteStride, outputByteStride = outputByteStride, indicesOffset = indicesAttrData.inputOffset, output = (byte*)output.GetUnsafePtr() }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); return job; } static unsafe JobHandle CreateConvertGenericAttributeJob( AttributeData attrData, uint inputByteStride, uint outputByteStride, int vertexCount, NativeArray inputStream, NativeArray outputStream ) { var job = new ExportJobs.ConvertGenericJob { inputByteStride = inputByteStride, outputByteStride = outputByteStride, byteLength = (uint)attrData.Size, input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.inputOffset, output = (byte*)outputStream.GetUnsafePtr() + attrData.outputOffset }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); return job; } static DrawMode? GetDrawMode(MeshTopology topology) { switch (topology) { case MeshTopology.Quads: return DrawMode.Triangles; case MeshTopology.Triangles: return DrawMode.Triangles; case MeshTopology.Lines: return DrawMode.Lines; case MeshTopology.LineStrip: return DrawMode.LineStrip; case MeshTopology.Points: return DrawMode.Points; default: return null; } } Node AddChildNode( int parentId, float3? translation = null, quaternion? rotation = null, float3? scale = null, string name = null ) { var parent = m_Nodes[parentId]; var node = CreateNode(translation, rotation, scale, name); m_Nodes.Add(node); var nodeId = (uint)m_Nodes.Count - 1; if (parent.children == null) { parent.children = new[] { nodeId }; } else { var newChildren = new uint[parent.children.Length + 1]; newChildren[0] = nodeId; parent.children.CopyTo(newChildren, 1); parent.children = newChildren; } return node; } static Node CreateNode( float3? translation = null, quaternion? rotation = null, float3? scale = null, string name = null ) { var node = new Node { name = name, }; if (translation.HasValue && !translation.Equals(float3.zero)) { node.translation = new[] { -translation.Value.x, translation.Value.y, translation.Value.z }; } if (rotation.HasValue && !rotation.Equals(quaternion.identity)) { node.rotation = new[] { rotation.Value.value.x, -rotation.Value.value.y, -rotation.Value.value.z, rotation.Value.value.w }; } if (scale.HasValue && !scale.Equals(new float3(1f))) { node.scale = new[] { scale.Value.x, scale.Value.y, scale.Value.z }; } return node; } int AddMesh(UnityEngine.Mesh uMesh, VertexAttributeUsage attributeUsage) { int meshId; if (!uMesh.isReadable) { #if DEBUG && !UNITY_6000_0_OR_NEWER Debug.LogWarning($"Exporting non-readable meshes is not reliable in builds across platforms and " + $"graphics APIs! Consider making mesh \"{uMesh.name}\" readable.", uMesh); #endif // Unity 2020 and older does not support accessing non-readable meshes via GraphicsBuffer. #if UNITY_2021_3_OR_NEWER // As of now Draco for Unity does not support encoding non-readable meshes. if ((m_Settings.Compression & Compression.Draco) != 0) #endif { #if UNITY_2021_3_OR_NEWER && UNITY_EDITOR // Non-readable meshes are unsupported during playmode or in builds, but work in Editor exports. if (Application.isPlaying) #endif { m_Logger?.Error(LogCode.MeshNotReadable, uMesh.name); return -1; } } } if (m_UnityMeshes != null) { meshId = m_UnityMeshes.IndexOf(uMesh); if (meshId >= 0) { SetVertexAttributeUsage(meshId, attributeUsage); return meshId; } } var mesh = new Mesh { name = uMesh.name }; m_Meshes = m_Meshes ?? new List(); m_UnityMeshes = m_UnityMeshes ?? new List(); m_MeshVertexAttributeUsage ??= new List(); m_Meshes.Add(mesh); m_UnityMeshes.Add(uMesh); m_MeshVertexAttributeUsage.Add(attributeUsage); meshId = m_Meshes.Count - 1; return meshId; } int AddSkin(int meshId, uint[] joints) { m_Skins ??= new List(); m_SkinMesh ??= new List(); var skinId = m_Skins.Count; var newSkin = new Skin { joints = joints }; m_Skins.Add(newSkin); m_SkinMesh.Add(meshId); return skinId; } unsafe int WriteBufferViewToBuffer(byte[] bufferViewData, BufferViewTarget target, int? byteStride = null) { var bufferHandle = GCHandle.Alloc(bufferViewData, GCHandleType.Pinned); fixed (void* bufferAddress = &bufferViewData[0]) { var nativeData = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(bufferAddress, bufferViewData.Length, Allocator.None); #if ENABLE_UNITY_COLLECTIONS_CHECKS var safetyHandle = AtomicSafetyHandle.Create(); NativeArrayUnsafeUtility.SetAtomicSafetyHandle(array: ref nativeData, safetyHandle); #endif var bufferViewId = WriteBufferViewToBuffer(nativeData, target, byteStride); #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckDeallocateAndThrow(safetyHandle); AtomicSafetyHandle.Release(safetyHandle); #endif bufferHandle.Free(); return bufferViewId; } } Stream CertifyBuffer() { if (m_BufferStream == null) { // Delayed, implicit stream generation. // if `m_BufferPath` was set, we need a FileStream if (m_BufferPath != null) { m_BufferStream = new FileStream(m_BufferPath, FileMode.Create); } else { m_BufferStream = new MemoryStream(); } } return m_BufferStream; } /// /// Writes the given data to the main buffer, creates a bufferView and returns its index /// /// Content to write to buffer /// Target of the bufferView /// The byte size of an element. Provide it, /// if it cannot be inferred from the accessor /// If not zero, the offsets of the bufferView /// will be multiple of it to please alignment rules (padding bytes will be added, /// if required; see https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#data-alignment ) /// /// Buffer view index int WriteBufferViewToBuffer(NativeArray bufferViewData, BufferViewTarget bufferViewTarget, int? byteStride = null, int byteAlignment = 0) { Profiler.BeginSample("WriteBufferViewToBuffer"); var buffer = CertifyBuffer(); var byteOffset = buffer.Length; if (byteAlignment > 0) { Assert.IsTrue(byteAlignment < 5); // There is no componentType that requires more than 4 bytes var alignmentByteCount = (byteAlignment - (byteOffset % byteAlignment)) % byteAlignment; for (var i = 0; i < alignmentByteCount; i++) { buffer.WriteByte(0); } // Update byteOffset byteOffset = buffer.Length; } buffer.Write(bufferViewData); var bufferView = new BufferView { buffer = 0, byteOffset = (int)byteOffset, byteLength = bufferViewData.Length, target = (int)bufferViewTarget, }; if (byteStride.HasValue) { // Adhere data alignment rules Assert.IsTrue(byteStride.Value % 4 == 0); bufferView.byteStride = byteStride.Value; } m_BufferViews = m_BufferViews ?? new List(); var bufferViewId = m_BufferViews.Count; m_BufferViews.Add(bufferView); Profiler.EndSample(); return bufferViewId; } void SetVertexAttributeUsage(int meshId, VertexAttributeUsage attributeUsage) { var existingUsage = m_MeshVertexAttributeUsage[meshId]; if (((existingUsage ^ attributeUsage) & VertexAttributeUsage.Color) == VertexAttributeUsage.Color) { m_Logger.Warning(LogCode.InconsistentVertexColorUsage, meshId.ToString()); } m_MeshVertexAttributeUsage[meshId] = attributeUsage | existingUsage; } void Dispose() { m_Settings = null; m_Logger = null; m_Gltf = null; m_ExtensionsUsedOnly = null; m_ExtensionsRequired = null; m_ImageExports = null; m_SamplerKeys = null; m_UnityMaterials = null; m_UnityMeshes = null; m_MeshVertexAttributeUsage = null; m_NodeMaterials = null; m_BufferStream?.Close(); m_BufferStream = null; m_BufferPath = null; m_Scenes = null; m_Nodes = null; m_Meshes = null; m_Accessors = null; m_BufferViews = null; m_Materials = null; m_Images = null; m_Textures = null; m_Samplers = null; m_State = State.Disposed; } static unsafe int GetAttributeSize(VertexAttributeFormat format) { switch (format) { case VertexAttributeFormat.Float32: return sizeof(float); case VertexAttributeFormat.Float16: return sizeof(half); case VertexAttributeFormat.UNorm8: return sizeof(byte); case VertexAttributeFormat.SNorm8: return sizeof(sbyte); case VertexAttributeFormat.UNorm16: return sizeof(ushort); case VertexAttributeFormat.SNorm16: return sizeof(short); case VertexAttributeFormat.UInt8: return sizeof(byte); case VertexAttributeFormat.SInt8: return sizeof(sbyte); case VertexAttributeFormat.UInt16: return sizeof(ushort); case VertexAttributeFormat.SInt16: return sizeof(short); case VertexAttributeFormat.UInt32: return sizeof(uint); case VertexAttributeFormat.SInt32: return sizeof(int); default: throw new ArgumentOutOfRangeException(nameof(format), format, null); } } static VertexAttributeUsage GetVertexAttributeUsage(Shader shader) { var shaderName = shader.name; if (shaderName.EndsWith("unlit", StringComparison.InvariantCultureIgnoreCase)) { return VertexAttributeUsage.Position // Only two UV channels | VertexAttributeUsage.TwoTexCoords | VertexAttributeUsage.Color | VertexAttributeUsage.Skinning; } if (shaderName.StartsWith("Shader Graphs/glTF-", StringComparison.InvariantCulture) || shaderName.StartsWith("glTF/", StringComparison.InvariantCulture) || shaderName.StartsWith("Particles/Standard", StringComparison.InvariantCulture) ) { return VertexAttributeUsage.Position | VertexAttributeUsage.Normal | VertexAttributeUsage.Tangent // Only two UV channels | VertexAttributeUsage.TwoTexCoords | VertexAttributeUsage.Color | VertexAttributeUsage.Skinning; } // Note: No vertex colors. Most shaders don't make use of them, so discard them by default. return VertexAttributeUsage.Position | VertexAttributeUsage.Normal | VertexAttributeUsage.Tangent | VertexAttributeUsage.AllTexCoords | VertexAttributeUsage.Skinning; } } }