// SPDX-FileCopyrightText: 2023 Unity Technologies and the glTFast authors // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using UnityEngine.Assertions; using UnityEngine.Profiling; namespace GLTFast.Schema { /// [Serializable] public class Root : RootBase< Accessor, Animation, Asset, Buffer, BufferView, Camera, RootExtensions, Image, Material, Mesh, Node, Sampler, Scene, Skin, Texture > { } /// /// Accessor type /// Animation type /// Asset type /// Buffer type /// BufferView type /// Camera type /// Extensions type /// Image type /// Material type /// Mesh type /// Node type /// Sampler type /// Scene type /// Skin type /// Texture type [Serializable] public abstract class RootBase< TAccessor, TAnimation, TAsset, TBuffer, TBufferView, TCamera, TExtensions, TImage, TMaterial, TMesh, TNode, TSampler, TScene, TSkin, TTexture > : RootBase where TAccessor : AccessorBase where TAnimation : AnimationBase where TAsset : Asset where TBuffer : Buffer where TBufferView : BufferViewBase where TCamera : CameraBase where TExtensions : RootExtensions where TImage : Image where TMaterial : MaterialBase where TMesh : MeshBase where TNode : NodeBase where TSampler : Sampler where TScene : Scene where TSkin : Skin where TTexture : TextureBase { /// public TAccessor[] accessors; #if UNITY_ANIMATION /// public TAnimation[] animations; #endif /// public TAsset asset; /// public TBuffer[] buffers; /// public TBufferView[] bufferViews; /// public TCamera[] cameras; /// public TImage[] images; /// public TMaterial[] materials; /// public TNode[] nodes; /// public TSampler[] samplers; /// public TScene[] scenes; /// public TSkin[] skins; /// public TTexture[] textures; /// public TExtensions extensions; /// public TMesh[] meshes; /// public override IReadOnlyList Accessors => accessors; #if UNITY_ANIMATION /// public override IReadOnlyList Animations => animations; #endif /// public override Asset Asset => asset; /// public override IReadOnlyList Buffers => buffers; /// public override IReadOnlyList BufferViews => bufferViews; /// public override IReadOnlyList Cameras => cameras; /// public override IReadOnlyList Images => images; /// public override IReadOnlyList Materials => materials; /// public override IReadOnlyList Nodes => nodes; /// public override IReadOnlyList Samplers => samplers; /// public override IReadOnlyList Scenes => scenes; /// public override IReadOnlyList Skins => skins; /// public override IReadOnlyList Textures => textures; /// public override RootExtensions Extensions => extensions; /// internal override void UnsetExtensions() { extensions = null; } /// public override IReadOnlyList Meshes => meshes; } /// /// The root object for a glTF asset. /// /// [Serializable] public abstract class RootBase { /// /// Names of glTF extensions used somewhere in this asset. /// public string[] extensionsUsed; /// /// Names of glTF extensions required to properly load this asset. /// public string[] extensionsRequired; /// /// An array of accessors. An accessor is a typed view into a bufferView. /// public abstract IReadOnlyList Accessors { get; } #if UNITY_ANIMATION /// /// An array of keyframe animations. /// public abstract IReadOnlyList Animations { get; } #endif /// /// Metadata about the glTF asset. /// public abstract Asset Asset { get; } /// /// An array of buffers. A buffer points to binary geometry, animation, or skins. /// public abstract IReadOnlyList Buffers { get; } /// /// An array of bufferViews. /// A bufferView is a view into a buffer generally representing a subset of the buffer. /// public abstract IReadOnlyList BufferViews { get; } /// /// An array of cameras. A camera defines a projection matrix. /// public abstract IReadOnlyList Cameras { get; } /// /// An array of images. An image defines data used to create a texture. /// public abstract IReadOnlyList Images { get; } /// /// An array of materials. A material defines the appearance of a primitive. /// public abstract IReadOnlyList Materials { get; } /// /// An array of meshes. A mesh is a set of primitives to be rendered. /// public abstract IReadOnlyList Meshes { get; } /// /// An array of nodes. /// public abstract IReadOnlyList Nodes { get; } /// /// An array of samplers. A sampler contains properties for texture filtering and wrapping modes. /// public abstract IReadOnlyList Samplers { get; } /// /// The index of the default scene. /// public int scene = -1; /// /// An array of scenes. /// public abstract IReadOnlyList Scenes { get; } /// /// An array of skins. A skin is defined by joints and matrices. /// public abstract IReadOnlyList Skins { get; } /// /// An array of textures. /// public abstract IReadOnlyList Textures { get; } /// public abstract RootExtensions Extensions { get; } /// /// Sets to null. /// internal abstract void UnsetExtensions(); #if UNITY_ANIMATION public bool HasAnimation => Animations != null && Animations.Count > 0; #endif // UNITY_ANIMATION /// /// Looks up if a certain accessor points to interleaved data. /// /// Accessor index /// True if accessor is interleaved, false if its data is /// continuous. public bool IsAccessorInterleaved(int accessorIndex) { var accessor = Accessors[accessorIndex]; var bufferView = BufferViews[accessor.bufferView]; if (bufferView.byteStride < 0) return false; return bufferView.byteStride > accessor.ElementByteSize; } /// /// Serialization to JSON /// /// Stream the JSON string is being written to. public void GltfSerialize(StreamWriter stream) { var writer = new JsonWriter(stream); if (Asset != null) { writer.AddProperty("asset"); Asset.GltfSerialize(writer); } if (Nodes != null) { writer.AddArray("nodes"); foreach (var node in Nodes) { node.GltfSerialize(writer); } writer.CloseArray(); } if (extensionsRequired != null) { writer.AddArrayProperty("extensionsRequired", extensionsRequired); } if (extensionsUsed != null) { writer.AddArrayProperty("extensionsUsed", extensionsUsed); } #if UNITY_ANIMATION if (Animations!=null) { writer.AddArray("animations"); foreach( var animation in Animations) { animation.GltfSerialize(writer); } writer.CloseArray(); } #endif if (Buffers != null) { writer.AddArray("buffers"); foreach (var buffer in Buffers) { buffer.GltfSerialize(writer); } writer.CloseArray(); } if (BufferViews != null) { writer.AddArray("bufferViews"); foreach (var bufferView in BufferViews) { bufferView.GltfSerialize(writer); } writer.CloseArray(); } if (Accessors != null) { writer.AddArray("accessors"); foreach (var accessor in Accessors) { accessor.GltfSerialize(writer); } writer.CloseArray(); } if (Cameras != null) { writer.AddArray("cameras"); foreach (var camera in Cameras) { camera.GltfSerialize(writer); } writer.CloseArray(); } if (Images != null) { writer.AddArray("images"); foreach (var image in Images) { image?.GltfSerialize(writer); } writer.CloseArray(); } if (Materials != null) { writer.AddArray("materials"); foreach (var material in Materials) { material.GltfSerialize(writer); } writer.CloseArray(); } if (Meshes != null) { writer.AddArray("meshes"); foreach (var mesh in Meshes) { mesh.GltfSerialize(writer); } writer.CloseArray(); } if (Samplers != null) { writer.AddArray("samplers"); foreach (var sampler in Samplers) { sampler.GltfSerialize(writer); } writer.CloseArray(); } if (scene >= 0) { writer.AddProperty("scene", scene); } if (Scenes != null) { writer.AddArray("scenes"); foreach (var sceneToSerialize in Scenes) { sceneToSerialize.GltfSerialize(writer); } writer.CloseArray(); } if (Skins != null) { writer.AddArray("skins"); foreach (var skin in Skins) { skin.GltfSerialize(writer); } writer.CloseArray(); } if (Textures != null) { writer.AddArray("textures"); foreach (var texture in Textures) { texture.GltfSerialize(writer); } writer.CloseArray(); } if (Extensions != null) { writer.AddProperty("extensions"); Extensions.GltfSerialize(writer); } writer.Close(); } /// /// Detects if a secondary null-check is necessary. /// /// True if a secondary parse against the FakeSchema is required. False otherwise internal bool JsonUtilitySecondParseRequired() { Profiler.BeginSample("JsonUtilitySecondParseRequired"); var check = false; if (Materials != null) { foreach (var mat in Materials) { // mat.extension is always set (not null), because JsonUtility constructs a default // if any of mat.extension's members is not null, it is because there was // a legit extensions node in JSON => we have to check which ones if (mat.Extensions.KHR_materials_unlit != null) { check = true; } else { // otherwise dump the wrongfully constructed MaterialExtension mat.UnsetExtensions(); } } } if (Accessors != null) { foreach (var accessor in Accessors) { if (accessor.Sparse.Indices == null || accessor.Sparse.Values == null) { // If indices and values members are null, `sparse` is likely // an auto-instance by the JsonUtility and not present in JSON. // Therefore we remove it: accessor.UnsetSparse(); } #if GLTFAST_SAFE else { // This is very likely a valid sparse accessor. // However, an empty sparse property ( "sparse": {} ) would break // glTFast, so better do a thorough follow-up check check = true; } #endif // GLTFAST_SAFE } } #if DRACO_IS_INSTALLED if(!check && Meshes!=null) { foreach (var mesh in Meshes) { if (mesh.Primitives != null) { foreach (var primitive in mesh.Primitives) { if (primitive.Extensions?.KHR_draco_mesh_compression != null) { check = true; break; } } } } } #endif Profiler.EndSample(); return check; } internal void JsonUtilityCleanupAgainstSecondParse(FakeSchema.Root fakeRoot) { Profiler.BeginSample("JsonUtilityCleanup"); if (Materials != null) { for (var i = 0; i < Materials.Count; i++) { var mat = Materials[i]; if (mat.Extensions == null) continue; Assert.AreEqual(mat.name, fakeRoot.materials[i].name); var fake = fakeRoot.materials[i].extensions; if (fake.KHR_materials_unlit == null) { mat.Extensions.KHR_materials_unlit = null; } if (fake.KHR_materials_pbrSpecularGlossiness == null) { mat.Extensions.KHR_materials_pbrSpecularGlossiness = null; } if (fake.KHR_materials_transmission == null) { mat.Extensions.KHR_materials_transmission = null; } if (fake.KHR_materials_clearcoat == null) { mat.Extensions.KHR_materials_clearcoat = null; } if (fake.KHR_materials_sheen == null) { mat.Extensions.KHR_materials_sheen = null; } if (fake.KHR_materials_ior == null) { mat.Extensions.KHR_materials_ior = null; } if (fake.KHR_materials_specular == null) { mat.Extensions.KHR_materials_specular = null; } } } #if GLTFAST_SAFE if (Accessors != null) { for (var i = 0; i < Accessors.Count; i++) { var sparse = fakeRoot.accessors[i].sparse; if (sparse?.indices == null || sparse.values == null) { Accessors[i].UnsetSparse(); } } } #endif if (Meshes != null) { for (var i = 0; i < Meshes.Count; i++) { var mesh = Meshes[i]; Assert.AreEqual(mesh.name, fakeRoot.meshes[i].name); for (var j = 0; j < mesh.Primitives.Count; j++) { var primitive = mesh.Primitives[j]; if (primitive.Extensions == null) continue; var fake = fakeRoot.meshes[i].primitives[j]; #if DRACO_IS_INSTALLED if (fake.extensions.KHR_draco_mesh_compression == null) { primitive.Extensions.KHR_draco_mesh_compression = null; } #endif if (fake.extensions.KHR_materials_variants == null) { primitive.Extensions.KHR_materials_variants = null; } } } } Profiler.EndSample(); } /// /// Cleans up invalid parsing artifacts created by . /// If you inherit a custom Root class (for use with /// /// ) you can override this method to perform sanity checks on the deserialized, custom properties. /// public virtual void JsonUtilityCleanup() { if (Nodes != null) { foreach (var t in Nodes) { t.JsonUtilityCleanup(); } } if (Extensions != null && !Extensions.JsonUtilityCleanup()) { UnsetExtensions(); } if (Textures != null) { foreach (var texture in Textures) { texture.JsonUtilityCleanup(); } } } /// /// Number of materials variants. /// /// public int MaterialsVariantsCount => Extensions?.KHR_materials_variants?.variants?.Count ?? 0; /// /// Gets the name of a specific materials variant. /// /// Materials variant index. /// Name of a materials variant. /// public string GetMaterialsVariantName(int index) { var variants = Extensions?.KHR_materials_variants?.variants; if (variants != null && index >= 0 && index < variants.Count) { return variants[index].name; } return null; } } }