commit 8256444c7657c5d15a6aab4f2354f6477fe53a7f Author: H. Utku Maden Date: Sun Oct 12 14:33:16 2025 +0300 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25dd304 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# common editors +**/.vs +**/.vscode +**/.rider +**/.atom +**/.idea + +# build artifacts +**/obj +**/bin + +doc/html/** +doc/latex/** + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/ReFuel.Gltf.sln b/ReFuel.Gltf.sln new file mode 100644 index 0000000..80cc54e --- /dev/null +++ b/ReFuel.Gltf.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReFuel.Gltf", "ReFuel.Gltf\ReFuel.Gltf.csproj", "{7DDA6046-BC8D-4969-BF6F-8450D515E8AE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7DDA6046-BC8D-4969-BF6F-8450D515E8AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DDA6046-BC8D-4969-BF6F-8450D515E8AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DDA6046-BC8D-4969-BF6F-8450D515E8AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DDA6046-BC8D-4969-BF6F-8450D515E8AE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReFuel.Gltf/GlbStructures.cs b/ReFuel.Gltf/GlbStructures.cs new file mode 100755 index 0000000..30d41eb --- /dev/null +++ b/ReFuel.Gltf/GlbStructures.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace ReFuel.Gltf +{ + [StructLayout(LayoutKind.Explicit, Size = 12)] + public struct GlbHeader + { + [FieldOffset(0)] + public int Signature; + [FieldOffset(4)] + public int Version; + [FieldOffset(8)] + public int Length; + + public const int SIGNATURE_VALUE = 0x46546C67; // "glTF" + + public static unsafe bool Read(Stream str, out GlbHeader header) + { + header = default; + int len; + + fixed (GlbHeader* pheader = &header) + len = str.Read(new Span(pheader, sizeof(GlbHeader))); + + if (len != sizeof(GlbHeader)) + return false; + else if (header.Signature == GlbHeader.SIGNATURE_VALUE) + return true; + else + return false; + } + } + + [StructLayout(LayoutKind.Explicit, Size = 8)] + public struct GlbChunk + { + [FieldOffset(0)] + public int Size; + [FieldOffset(4)] + public GlbChunkType Type; + + public static unsafe bool Read(Stream str, out GlbChunk chunk) + + { + chunk = default; + + fixed (GlbChunk* pchunk = &chunk) + return str.Read(new Span(pchunk, sizeof(GlbChunk))) == sizeof(GlbChunk); + } + } + + public enum GlbChunkType : int + { + Json = 0x4E4F534A, + Bin = 0x004E4942, + } +} diff --git a/ReFuel.Gltf/GltfAccessor.cs b/ReFuel.Gltf/GltfAccessor.cs new file mode 100755 index 0000000..b9d2560 --- /dev/null +++ b/ReFuel.Gltf/GltfAccessor.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public class GltfAccessor : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.Accessor; + private GltfSmartId bufferView; + + public GltfBufferView? BufferView + { + get => bufferView.IsSet ? bufferView.Value : null; + set + { + if (value is null) + bufferView.Reset(); + else + bufferView.Value = value; + } + } + + public long ByteOffset { get; set; } = 0; + public GltfComponentType ComponentType { get; set; } = GltfComponentType.Float; + public bool Normalized { get; set; } = false; + public int Count { get; set; } = 0; + public GltfAccessorType Type { get; set; } = GltfAccessorType.Scalar; + public float[]? Max { get; set; } = null; + public float[]? Min { get; set; } = null; + public object? Sparse { get; set; } = null; + public string? Name { get; set; } = null; + + public GltfAccessor(GltfDocument document) : base(document) + { + bufferView = new GltfSmartId(document.BufferViews); + } + + internal override void Deserialize(JsonElement element) + { + AssertObject(element); + + if (element.TryGetProperty("bufferView", out JsonElement viewElement)) + { + if (!viewElement.TryGetInt32(out int viewId)) + { + PropertyFormatFail("bufferView", "expected an integer."); + } + else + { + bufferView.Id = viewId; + } + } + + if (element.TryGetProperty("byteOffset", out JsonElement offsetElement)) + { + if (!offsetElement.TryGetInt64(out long offset)) + { + PropertyFormatFail("byteOffset", "expected an integer."); + } + else + { + ByteOffset = offset; + } + } + + if (!element.TryGetProperty("componentType", out JsonElement componentElement)) + { + RequiredProperty("componentType"); + } + else if (!componentElement.TryGetInt32(out int i)) + { + PropertyFormatFail("componentType", "expected an integer."); + } + else + { + ComponentType = (GltfComponentType)i; + } + + if (element.TryGetProperty("normalized", out JsonElement normalizedElement)) + { + switch (normalizedElement.ValueKind) + { + case JsonValueKind.True: + Normalized = true; + break; + case JsonValueKind.False: + Normalized = false; + break; + default: + PropertyFormatFail("normalized", "expected a boolean."); + break; + } + } + + if (!element.TryGetProperty("count", out JsonElement countElement)) + RequiredProperty("count"); + else if (!countElement.TryGetInt32(out int i)) + PropertyFormatFail("count", "expected an integer."); + else + Count = i; + + if (!element.TryGetProperty("type", out JsonElement typeElement) ) + RequiredProperty("type"); + else + { + switch (typeElement.GetString()) + { + case "SCALAR": + Type = GltfAccessorType.Scalar; + break; + case "VEC2": + Type = GltfAccessorType.Vec2; + break; + case "VEC3": + Type = GltfAccessorType.Vec3; + break; + case "VEC4": + Type = GltfAccessorType.Vec4; + break; + + case "MAT2": + Type = GltfAccessorType.Mat2; + break; + case "MAT3": + Type = GltfAccessorType.Mat3; + break; + case "MAT4": + Type = GltfAccessorType.Mat4; + break; + + default: + RequiredProperty("type"); + break; + } + } + + if (element.TryGetProperty("min", out JsonElement minElement)) + { + if (minElement.ValueKind != JsonValueKind.Array) + PropertyFormatFail("min", "expected an array."); + + List floats = new List(); + + foreach (JsonElement child in minElement.EnumerateArray()) + { + if (!child.TryGetSingle(out float f)) + PropertyFormatFail("min", "expected numeric array members."); + floats.Add(f); + } + + Min = floats.ToArray(); + } + + if (element.TryGetProperty("max", out JsonElement maxElement)) + { + if (minElement.ValueKind != JsonValueKind.Array) + PropertyFormatFail("max", "expected an array."); + + List floats = new List(); + + foreach (JsonElement child in minElement.EnumerateArray()) + { + if (!child.TryGetSingle(out float f)) + PropertyFormatFail("max", "expected numeric array members."); + floats.Add(f); + } + + Max = floats.ToArray(); + } + + // TODO: I'm skipping sparse for now. + + if (element.TryGetProperty("name", out JsonElement nameElement)) + { + Name = nameElement.GetString(); + } + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/ReFuel.Gltf/GltfAccessorType.cs b/ReFuel.Gltf/GltfAccessorType.cs new file mode 100755 index 0000000..28b4484 --- /dev/null +++ b/ReFuel.Gltf/GltfAccessorType.cs @@ -0,0 +1,13 @@ +namespace ReFuel.Gltf +{ + public enum GltfAccessorType + { + Scalar, + Vec2, + Vec3, + Vec4, + Mat2, + Mat3, + Mat4 + } +} diff --git a/ReFuel.Gltf/GltfAsset.cs b/ReFuel.Gltf/GltfAsset.cs new file mode 100755 index 0000000..facaa59 --- /dev/null +++ b/ReFuel.Gltf/GltfAsset.cs @@ -0,0 +1,68 @@ +using System; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public class GltfAsset : GltfObject + { + public override GltfObjectKind Kind => GltfObjectKind.Asset; + public Version Version { get; private set; } = new Version(2, 0); + public Version? MinVersion { get; private set; } = null; + public string? Generator { get; private set; } = null; + public string? Copyright { get; private set; } = null; + + internal GltfAsset(GltfDocument document) : base(document) + { + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected a JSON object."); + + JsonElement value; + if (element.TryGetProperty("version", out value)) + { + string? str = value.GetString(); + if (Version.TryParse(str, out Version? v)) + { + Version = v; + } + else + { + throw new Exception("Malformed version string."); + } + } + + if (element.TryGetProperty("minVersion", out value)) + { + string? str = value.GetString(); + if (Version.TryParse(str, out Version? v)) + { + MinVersion = v; + } + else + { + throw new Exception("Malformed version string."); + } + } + + if (element.TryGetProperty("generator", out value)) + { + Generator = value.GetString(); + } + + if (element.TryGetProperty("copyright", out value)) + { + Copyright = value.GetString(); + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/ReFuel.Gltf/GltfAttributeType.cs b/ReFuel.Gltf/GltfAttributeType.cs new file mode 100755 index 0000000..2e59e6b --- /dev/null +++ b/ReFuel.Gltf/GltfAttributeType.cs @@ -0,0 +1,14 @@ +namespace ReFuel.Gltf +{ + public enum GltfAttributeType + { + Position, + Normal, + Tangent, + TexCoord, + Color, + Joints, + Weights, + Custom + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/GltfBuffer.cs b/ReFuel.Gltf/GltfBuffer.cs new file mode 100755 index 0000000..f19f779 --- /dev/null +++ b/ReFuel.Gltf/GltfBuffer.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using System.Text.Json; +using ReFuel.Gltf.IO; + +namespace ReFuel.Gltf +{ + public class GltfBuffer : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.Buffer; + + public Uri? Uri { get; set; } + public long ByteLength { get; set; } + public string? Name { get; set; } + + public GltfBuffer(GltfDocument document) : base(document) + { + } + + public Stream Open(string? pwd = null, IGltfStreamProvider? provider = null) + { + if (Uri == null) + { + return ((StreamWrapper)Document.BinaryStreams[Id]).Fork(keepOpen: true); + } + + switch (Uri.IsAbsoluteUri ? Uri.Scheme : "file") // Assume relative URIs are files. + { + case "file": + if (provider != null) + { + return provider.OpenFile(pwd, Uri) ?? throw new FileNotFoundException(Uri.ToString()); + } + else + { + string path = Path.Combine(pwd ?? string.Empty, Uri.LocalPath); + return File.OpenRead(path); + } + case "data": + return new GltfUriReader(Uri); + default: + if (provider != null) + return provider.OpenFile(pwd, Uri) ?? throw new FileNotFoundException(Uri.ToString()); + break; + } + + throw new Exception("Unrecognized URI scheme."); + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected an object."); + + if (element.TryGetProperty("byteLength", out JsonElement lengthElement)) + { + if (!lengthElement.TryGetInt64(out long length)) + throw new Exception("Length is not an integer."); + + ByteLength = length; + } + else + { + throw new Exception("Byte length is required."); + } + + if (element.TryGetProperty("name", out JsonElement nameElement)) + { + Name = nameElement.GetString(); + } + + if (element.TryGetProperty("uri", out JsonElement uriElement)) + { + if (!Uri.TryCreate(uriElement.GetString(), UriKind.RelativeOrAbsolute, out Uri? uri)) + throw new Exception("Malformed URI."); + + Uri = uri; + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/ReFuel.Gltf/GltfBufferView.cs b/ReFuel.Gltf/GltfBufferView.cs new file mode 100755 index 0000000..fa2621b --- /dev/null +++ b/ReFuel.Gltf/GltfBufferView.cs @@ -0,0 +1,96 @@ +using System; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public class GltfBufferView : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.BufferView; + + private GltfSmartId buffer; + + public GltfBuffer Buffer + { + get => buffer.Value; + set => buffer.Value = value; + } + + public long ByteOffset { get; set; } = 0; + public long ByteLength { get; set; } = 0; + public long Stride { get; set; } = 0; + public GltfTargetHint Target { get; set; } = GltfTargetHint.None; + public string? Name { get; set; } + + internal GltfBufferView(GltfDocument document) : base(document) + { + buffer = new GltfSmartId(document.Buffers); + } + + internal override void Deserialize(JsonElement element) + { + AssertObject(element); + + if (!element.TryGetProperty("buffer", out JsonElement bufferElement)) + { + RequiredProperty("buffer"); + } + else if (!bufferElement.TryGetInt32(out int bufferId)) + { + PropertyFormatFail("buffer", "expected an integer"); + } + else + { + buffer.Id = bufferId; + } + + if (!element.TryGetProperty("byteLength", out JsonElement byteLengthElement)) + { + RequiredProperty("byteLength"); + } + else if (!byteLengthElement.TryGetInt64(out long byteLength)) + { + PropertyFormatFail("byteLength", "expected an integer"); + } + else + { + ByteLength = byteLength; + } + + if (element.TryGetProperty("byteOffset", out JsonElement byteOffset)) + { + if (!byteOffset.TryGetInt64(out long l)) + PropertyFormatFail("byteOffset", "excpected an integer."); + + ByteOffset = l; + } + + if (element.TryGetProperty("byteStride", out JsonElement stride)) + { + if (!stride.TryGetInt64(out long l)) + PropertyFormatFail("byteStride", "excpected an integer."); + + Stride = l; + } + + if (element.TryGetProperty("target", out JsonElement targetElement)) + { + if (!targetElement.TryGetInt32(out int i)) + PropertyFormatFail("target", "expected an integer."); + + Target = (GltfTargetHint)i; + } + + if (element.TryGetProperty("name", out JsonElement nameElement)) + { + Name = nameElement.GetString(); + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/ReFuel.Gltf/GltfCollection.cs b/ReFuel.Gltf/GltfCollection.cs new file mode 100755 index 0000000..4a21f8a --- /dev/null +++ b/ReFuel.Gltf/GltfCollection.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + /// + /// An empty object for storing glTF extra and extension properties. + /// + public class GltfCollection : Dictionary + { + private bool extensionMode; + + public GltfCollection(bool extensionMode = false) + { + this.extensionMode = extensionMode; + } + + public void Parse(JsonElement collection) + { + if (collection.ValueKind != JsonValueKind.Object) + throw new Exception("Expected a JSON object."); + + foreach (JsonProperty property in collection.EnumerateObject()) + { + if (extensionMode && GltfDocument.TryGetExtensionProvider(property.Name, out IGltfExtensionProvider? provider)) + { + Add(property.Name, provider.ParseExtensionObject(property.Value)); + continue; + } + + switch (property.Value.ValueKind) + { + case JsonValueKind.Array: + GltfList list = new GltfList(); + list.Parse(property.Value); + Add(property.Name, list); + break; + case JsonValueKind.False: + Add(property.Name, false); + break; + case JsonValueKind.Null: + Add(property.Name, null); + break; + case JsonValueKind.Number: + // How do we decide if the thing we want is an integer or a fp value? + // Integer compare them... + property.Value.TryGetDouble(out double d); + property.Value.TryGetInt64(out long l); + if (l == (long)d) + { + Add(property.Name, l); + } + else + { + Add(property.Name, d); + } + break; + case JsonValueKind.Object: + GltfCollection child = new GltfCollection(extensionMode); + child.Parse(property.Value); + Add(property.Name, child); + break; + case JsonValueKind.True: + Add(property.Name, true); + break; + case JsonValueKind.String: + Add(property.Name, property.Value.GetString()); + break; + case JsonValueKind.Undefined: break; + } + } + + throw new NotImplementedException(); + } + } + + public class GltfList : List + { + public GltfList() + { + } + + public void Parse(JsonElement collection) + { + if (collection.ValueKind != JsonValueKind.Array) + throw new Exception("Expected a JSON array."); + + foreach (JsonElement element in collection.EnumerateArray()) + { + switch (element.ValueKind) + { + case JsonValueKind.Array: + GltfList list = new GltfList(); + list.Parse(element); + Add(list); + break; + case JsonValueKind.False: + Add(false); + break; + case JsonValueKind.Null: + Add(null); + break; + case JsonValueKind.Number: + // How do we decide if the thing we want is an integer or a fp value? + // Integer compare them... + element.TryGetDouble(out double d); + element.TryGetInt64(out long l); + if (l == (long)d) + { + Add(l); + } + else + { + Add(d); + } + break; + case JsonValueKind.Object: + GltfCollection child = new GltfCollection(false); + child.Parse(element); + Add(child); + break; + case JsonValueKind.True: + Add(true); + break; + case JsonValueKind.String: + Add(element.GetString()); + break; + case JsonValueKind.Undefined: break; + } + } + } + } +} diff --git a/ReFuel.Gltf/GltfComponentType.cs b/ReFuel.Gltf/GltfComponentType.cs new file mode 100755 index 0000000..673ce52 --- /dev/null +++ b/ReFuel.Gltf/GltfComponentType.cs @@ -0,0 +1,12 @@ +namespace ReFuel.Gltf +{ + public enum GltfComponentType + { + Byte = 5120, + UnsignedByte = 5121, + Short = 5122, + UnsignedShort = 5123, + UnsignedInt = 5125, + Float = 5126 + } +} diff --git a/ReFuel.Gltf/GltfDocument.cs b/ReFuel.Gltf/GltfDocument.cs new file mode 100755 index 0000000..b10a79d --- /dev/null +++ b/ReFuel.Gltf/GltfDocument.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using ReFuel.Gltf.IO; + +namespace ReFuel.Gltf +{ + [Flags] + public enum GltfDocumentKind + { + Json = 1 << 0, + Binary = 1 << 1, + Any = Json | Binary, + } + + + /// + /// glTF Document Object GLObjects + /// + [JsonConverter(typeof(GltfJsonConverter))] + public class GltfDocument : GltfObject, IDisposable + { + public override GltfObjectKind Kind => GltfObjectKind.Document; + + public List BinaryStreams { get; private set; } + + public int DefaultScene { get; set; } = 0; + public HashSet Extensions { get; set; } = new HashSet(); + public HashSet Required { get; set; } = new HashSet(); + public GltfAsset Asset { get; } + public GltfIdList Scenes { get; } = new GltfIdList(); + public GltfIdList Nodes { get; } = new GltfIdList(); + public GltfIdList Meshes { get; } = new GltfIdList(); + public GltfIdList Buffers { get; } = new GltfIdList(); + public GltfIdList BufferViews { get; } = new GltfIdList(); + public GltfIdList Accessors { get; } = new GltfIdList(); + public GltfIdList Materials { get; } = new GltfIdList(); + public GltfIdList Textures { get; } = new GltfIdList(); + public GltfIdList Samplers { get; } = new GltfIdList(); + public GltfIdList Images { get; } = new GltfIdList(); + public GltfIdList Skins { get; } = new GltfIdList(); + + private GltfDocument(JsonDocument doc, IEnumerable? binaryStreams = null) + { + Document = this; + Asset = new GltfAsset(this); + + if (binaryStreams == null) + BinaryStreams = new List(); + else + BinaryStreams = new List(binaryStreams); + + JsonElement root = doc.RootElement; + Deserialize(root); + } + + private static void WriteStringSet(HashSet set, JsonElement stringArray) + { + if (stringArray.ValueKind != JsonValueKind.Array) + throw new Exception("Expected a string array."); + + foreach (JsonElement item in stringArray.EnumerateArray()) + { + string? str = item.GetString(); + if (str != null) + set.Add(str); + } + } + + public void Save(Stream str, GltfDocumentKind kind) + { + if (BinaryStreams.Count > 0) + { + if (kind == GltfDocumentKind.Json) + throw new NotSupportedException("Cannot write binary streams into a JSON style glTF document."); + else if (kind == GltfDocumentKind.Any) + kind = GltfDocumentKind.Binary; + } + + switch (kind) + { + case GltfDocumentKind.Json: + SaveJson(str); + break; + default: + case GltfDocumentKind.Binary: + SaveBinary(str); + break; + } + } + + private void SaveJson(Stream str) + { + throw new NotImplementedException(); + } + + private void SaveBinary(Stream str) + { + throw new NotImplementedException(); + } + + private static GltfDocument OpenJson(StreamWrapper str) + { + JsonDocument doc = JsonDocument.Parse(str, new JsonDocumentOptions() + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip + }); + + return new GltfDocument(doc); + } + + internal static GltfDocument OpenJson(ref Utf8JsonReader reader) + { + if (!JsonDocument.TryParseValue(ref reader, out JsonDocument? document)) + { + throw new JsonException(); + } + + return new GltfDocument(document); + } + + private static GltfDocument OpenBinary(StreamWrapper str) + { + if (GlbHeader.Read(str, out GlbHeader header)) + { + return OpenBinary(str, header); + } + + throw new Exception("File does not have a valid GLB header."); + } + + private static GltfDocument OpenBinary(StreamWrapper str, in GlbHeader header) + { + if (header.Version < 2) + { + throw new NotSupportedException("Only glTF version 2.0 files are supported."); + } + + JsonDocument? document = null; + List binaryStreams = new List(); + + while (GlbChunk.Read(str, out GlbChunk chunk)) + { + using StreamWrapper substream = str.ForkAbs(str.Position, chunk.Size, true); + + if (chunk.Type == GlbChunkType.Json) + { + document = JsonDocument.Parse(substream); + } + else + { + Stream copy = new MemoryStream(); + + substream.CopyTo(copy); + copy.Position = 0; + + binaryStreams.Add(new StreamWrapper(copy)); + } + } + + if (document == null) + { + throw new Exception("This glTF document has no manifest JSON."); + } + else + { + return new GltfDocument(document, binaryStreams); + } + } + + public static GltfDocument Open(Stream str, GltfDocumentKind kind = GltfDocumentKind.Any) + { + using StreamWrapper wrapper = new StreamWrapper(str, true); + + switch (kind) + { + case GltfDocumentKind.Json: + return OpenJson(wrapper); + case GltfDocumentKind.Binary: + return OpenBinary(wrapper); + default: + case GltfDocumentKind.Any: + if (GlbHeader.Read(wrapper, out GlbHeader header)) + { + return OpenBinary(wrapper, header); + } + else + { + str.Seek(0, SeekOrigin.Begin); + return OpenJson(wrapper); + } + } + } + + private bool isDisposed = false; + + public void Dispose() => Dispose(true); + private void Dispose(bool disposing) + { + if (isDisposed) return; + + if (disposing) + { + foreach (Stream str in BinaryStreams) + str.Dispose(); + + GC.SuppressFinalize(this); + } + + isDisposed = true; + } + + private static Dictionary extensionProviders = new Dictionary(); + private static HashSet supportedExtensions = new HashSet(); + + public static IReadOnlySet SupportedExtensions => supportedExtensions; + + static GltfDocument() + { + // AddExtension(RfChecksumProvider.ExtensionProviders); + // AddExtension(RfCompression.ExtensionProviders); + } + + public static void AddExtension(IGltfExtensionProvider provider) + { + supportedExtensions.Add(provider.Name); + extensionProviders.Add(provider.Name, provider); + } + + public static void AddExtension() where T : IGltfExtensionProvider, new() + { + AddExtension(new T()); + } + + public static void AddExtension(IEnumerable providers) + { + foreach (var provider in providers) AddExtension(provider); + } + + public static bool TryGetExtensionProvider(string name, [NotNullWhen(true)] out IGltfExtensionProvider? provider) + { + return extensionProviders.TryGetValue(name, out provider); + } + + internal override void Deserialize(JsonElement root) + { + if (root.ValueKind != JsonValueKind.Object) + throw new Exception("The glTF JSON document has an object as its root element"); + + Dictionary elements = new Dictionary(); + foreach (JsonProperty property in root.EnumerateObject()) + { + // Some elements may be interpreted earlier than others. + // This is mainly the default child, asset information, and the extensions. + switch (property.Name) + { + case "asset": + Asset.Deserialize(property.Value); + if (Asset.MinVersion > new Version(2, 0) || Asset.Version != new Version(2, 0)) + throw new Exception($"Unsupported glTF version {Asset.MinVersion ?? Asset.Version}"); + break; + + case "extensionsUsed": + WriteStringSet(Extensions, property.Value); + break; + + case "requiredExtensions": + WriteStringSet(Required, property.Value); + break; + + case "child": + if (property.Value.TryGetInt32(out int defaultScene)) + DefaultScene = defaultScene; + break; + default: + elements.Add(property.Name, property.Value); + break; + } + } + + foreach (string value in Required) + { + if (!SupportedExtensions.Contains(value)) + { + throw new Exception($"Need unsupported extension {value} for this glTF file."); + } + } + + foreach ((string name, JsonElement element) in elements) + { + switch (name) + { + case "extension": Extension.Parse(element); break; + case "extra": Extras.Parse(element); break; + case "scenes": + AssertArray(element, "scenes"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfScene scene = new GltfScene(this); + scene.Deserialize(child); + Scenes.Append(scene); + } + break; + case "nodes": + AssertArray(element, "nodes"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfNode node = new GltfNode(this); + node.Deserialize(child); + Nodes.Append(node); + } + break; + case "buffers": + AssertArray(element, "buffers"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfBuffer buffer = new GltfBuffer(this); + buffer.Deserialize(child); + Buffers.Append(buffer); + } + break; + case "bufferViews": + AssertArray(element, "bufferViews"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfBufferView view = new GltfBufferView(this); + view.Deserialize(child); + BufferViews.Append(view); + } + break; + case "accessors": + AssertArray(element, "accessors"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfAccessor accessor = new GltfAccessor(this); + accessor.Deserialize(child); + Accessors.Append(accessor); + } + break; + case "meshes": + AssertArray(element, "meshes"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfMesh mesh = new GltfMesh(this); + mesh.Deserialize(child); + Meshes.Append(mesh); + } + break; + case "materials": + AssertArray(element, "materials"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfMaterial material = new GltfMaterial(this); + material.Deserialize(child); + Materials.Append(material); + } + break; + case "textures": + AssertArray(element, "textures"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfTexture texture = new GltfTexture(this); + texture.Deserialize(child); + Textures.Append(texture); + } + break; + case "samplers": + AssertArray(element, "samplers"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfSampler sampler = new GltfSampler(this); + sampler.Deserialize(child); + Samplers.Append(sampler); + } + break; + case "images": + AssertArray(element, "images"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfImage image = new GltfImage(this); + image.Deserialize(child); + Images.Append(image); + } + break; + case "skins": + AssertArray(element, "skins"); + foreach (JsonElement child in element.EnumerateArray()) + { + GltfSkin skin = new GltfSkin(this); + skin.Deserialize(element); + Skins.Append(skin); + } + break; + } + } + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/ReFuel.Gltf/GltfEmptyExtensionProvider.cs b/ReFuel.Gltf/GltfEmptyExtensionProvider.cs new file mode 100755 index 0000000..37e9d24 --- /dev/null +++ b/ReFuel.Gltf/GltfEmptyExtensionProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public class GltfEmptyExtensionProvider : IGltfExtensionProvider + { + public string Name { get; } + + public Type? DataType { get; } = null; + + public GltfEmptyExtensionProvider(string name) + { + Name = name; + } + + public object ParseExtensionObject(JsonElement element) + { + throw new NotSupportedException(); + } + + public void WriteExtensionObject(object obj, Utf8JsonWriter writer) + { + throw new NotSupportedException(); + } + } +} diff --git a/ReFuel.Gltf/GltfExtensions.cs b/ReFuel.Gltf/GltfExtensions.cs new file mode 100755 index 0000000..ffa8e42 --- /dev/null +++ b/ReFuel.Gltf/GltfExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; + +namespace ReFuel.Gltf +{ + public static class GltfExtensions + { + #region ReFuel Extensions + + public const string RF_compression = nameof(RF_compression); + public const string RF_compression_deflate = nameof(RF_compression_deflate); + public const string RF_checksum = nameof(RF_checksum); + public const string RF_checksum_sha256 = nameof(RF_checksum_sha256); + public const string RF_texture_qoi = nameof(RF_texture_qoi); + + #endregion + + #region Khronos Extensions + + public const string KHR_lighs_punctual = nameof(KHR_lighs_punctual); + + #endregion + + public static ImmutableHashSet BaseExtensions { get; } + + static GltfExtensions() + { + Type t = typeof(GltfExtensions); + IEnumerable fields = t.GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(x => x.FieldType == typeof(string)) + .Select(x => x.Name); + BaseExtensions = ImmutableHashSet.Create(fields.ToArray()); + } + } +} diff --git a/ReFuel.Gltf/GltfIdCollection.cs b/ReFuel.Gltf/GltfIdCollection.cs new file mode 100755 index 0000000..3138d57 --- /dev/null +++ b/ReFuel.Gltf/GltfIdCollection.cs @@ -0,0 +1,209 @@ +using System.Collections; +using System.Collections.Generic; + +namespace ReFuel.Gltf +{ + public abstract class GltfIdObject : GltfObject + { + public int Id { get; internal set; } + + public GltfIdObject(GltfDocument document) : base(document) + { + } + } + + public class GltfIdList : IReadOnlyList + where T : GltfIdObject + { + private readonly List items = new List(); + + public T this[int index] + { + get => items[index]; + } + + public int Count => items.Count; + + public int Append(T item) + { + int i = items.Count; + + item.Id = i; + items.Add(item); + + return i; + } + + public bool IsSet(int i) + { + return i >= 0 && i <= Count; + } + + public void RemoveAt(int index) + { + items.RemoveAt(index); + + for (int i = index; i < Count; i++) + { + items[i].Id = i; + } + } + + public IEnumerator GetEnumerator() + { + return items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + } + + public class GltfRefList : ICollection, IReadOnlyList where T : GltfIdObject + { + private readonly List items = new List(); + private readonly GltfIdList list; + + public IEnumerable Ids + { + get + { + foreach (ItemTuple tuple in items) + { + yield return tuple.Item?.Id ?? tuple.Id; + } + } + } + + public int Count => items.Count; + public bool IsReadOnly => false; + + public T this[int index] + { + get + { + ItemTuple item = items[index]; + + if (item.Item != null) + return item.Item; + + item.Item = list[item.Id]; + items[index] = item; + return item.Item; + } + } + + public GltfRefList(GltfIdList referencedList) + { + list = referencedList; + } + + public void Add(T item) + { + items.Add(new ItemTuple(item.Id, item)); + } + + public void Add(int item) + { + items.Add(new ItemTuple(item, null)); + } + + public void Clear() + { + items.Clear(); + } + + public bool Contains(T item) + { + return items.FindIndex(x => (x.Item?.Id ?? x.Id) == item.Id) != -1; + } + + public void CopyTo(T[] array, int arrayIndex) + { + for (int i = 0; i < Count; i++) + { + array[arrayIndex + i] = this[i]; + } + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < Count; i++) + { + yield return this[i]; + } + } + + public bool Remove(T item) + { + int idx = items.FindIndex(x => (x.Item?.Id ?? x.Id) == item.Id); + + if (idx == -1) + return false; + + items.RemoveAt(idx); + return true; + } + + public bool Remove(int item) + { + int idx = items.FindIndex(x => (x.Item?.Id ?? x.Id) == item); + + if (idx == -1) + return false; + + items.RemoveAt(idx); + return true; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private record struct ItemTuple(int Id, T? Item); + } + + public struct GltfSmartId where T : GltfIdObject + { + private readonly GltfIdList list; + private int id = -1; + private T? value = null; + + public int Id + { + get => value?.Id ?? id; + set + { + Reset(); + id = value; + } + } + + public T Value + { + get => value ?? (value = list[id]); + set + { + id = value.Id; + this.value = value; + } + } + + public bool IsSet => list.IsSet(id); + + public GltfSmartId(GltfIdList list) + { + this.list = list; + } + + public void Reset() + { + id = -1; + value = null; + } + + public static implicit operator T(GltfSmartId id) => id.Value; + } +} diff --git a/ReFuel.Gltf/GltfImage.cs b/ReFuel.Gltf/GltfImage.cs new file mode 100644 index 0000000..b2ce082 --- /dev/null +++ b/ReFuel.Gltf/GltfImage.cs @@ -0,0 +1,105 @@ +using System; +using System.IO; +using System.Text.Json; +using ReFuel.Gltf.IO; + +namespace ReFuel.Gltf +{ + public class GltfImage : GltfIdObject + { + private GltfSmartId _bufferView = new GltfSmartId(); + public override GltfObjectKind Kind => GltfObjectKind.Image; + + public string? Name { get; set; } + public Uri? Uri { get; set; } + public string? MediaType { get; set; } + + public GltfBufferView? BufferView + { + get => _bufferView.IsSet ? _bufferView.Value : null; + set + { + if (value is null) + { + _bufferView.Reset(); + } + else + { + _bufferView.Value = value; + } + } + } + + internal GltfImage(GltfDocument document) : base(document) + { + } + + public Stream Open(string? pwd = null, IGltfStreamProvider? provider = null) + { + if (Uri == null) + { + throw new Exception("Only images with a URI property are to be opened."); + } + + if (Uri.IsAbsoluteUri) + { + switch (Uri.Scheme) + { + case "file": + if (provider != null) + { + return provider.OpenFile(pwd, Uri) ?? throw new FileNotFoundException(Uri.ToString()); + } + else + { + string path = Path.Combine(pwd ?? string.Empty, Uri.LocalPath); + return File.OpenRead(path); + } + case "data": + return new GltfUriReader(Uri); + } + } + + // otherwise + + if (provider != null) + return provider.OpenFile(pwd, Uri) ?? throw new FileNotFoundException(Uri.ToString()); + else + return File.OpenRead(Path.Combine(pwd ?? string.Empty, Uri.LocalPath)); + + // throw new Exception("Unrecognized URI scheme."); + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected a JSON object for glTF image node."); + + foreach (JsonProperty property in element.EnumerateObject()) + { + switch (property.Name) + { + case "uri": + Uri = new Uri(property.Value.GetString() ?? throw new Exception("Expected a string."), UriKind.RelativeOrAbsolute); + break; + case "mimeType": + MediaType = property.Value.GetString() ?? throw new Exception("Expected a string."); + break; + case "bufferView": + _bufferView.Id = property.Value.GetInt32(); + break; + case "name": + Name = property.Value.GetString(); + break; + } + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/GltfJsonSerializer.cs b/ReFuel.Gltf/GltfJsonSerializer.cs new file mode 100644 index 0000000..3a18e88 --- /dev/null +++ b/ReFuel.Gltf/GltfJsonSerializer.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ReFuel.Gltf +{ + public class GltfJsonConverter : JsonConverter + { + public override GltfDocument? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert != typeof(GltfDocument)) + throw new JsonException(); + + return GltfDocument.OpenJson(ref reader); + } + + public override void Write(Utf8JsonWriter writer, GltfDocument value, JsonSerializerOptions options) + { + value.Serialize(writer); + } + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/GltfMaterial.cs b/ReFuel.Gltf/GltfMaterial.cs new file mode 100644 index 0000000..8b6d15a --- /dev/null +++ b/ReFuel.Gltf/GltfMaterial.cs @@ -0,0 +1,313 @@ +using System; +using System.Drawing; +using System.Numerics; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public enum GltfMaterialAlphaMode + { + Opaque, + Mask, + Blend, + Other + } + + public class GltfMaterialTextureInfo : GtlfMiscObject + { + private GltfSmartId _texture; + + public GltfTexture? Texture + { + get => _texture.IsSet ? _texture.Value : null; + set + { + if (value is null) + _texture.Reset(); + else + _texture.Value = value; + } + } + + public int TexCoordIndex { get; set; } = 0; + + internal GltfMaterialTextureInfo(GltfDocument document) : base (document) + { + _texture = new GltfSmartId(document.Textures); + } + + public GltfMaterialTextureInfo(GltfDocument document, GltfTexture? texture, int texCoordIndex) : this(document) + { + Texture = texture; + TexCoordIndex = texCoordIndex; + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected an object."); + + if (element.TryGetProperty("index", out JsonElement indexProp)) + { + _texture.Id = indexProp.GetInt32(); + } + + if (element.TryGetProperty("texCoord", out JsonElement texCoordProp)) + { + TexCoordIndex = texCoordProp.GetInt32(); + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } + + public class GltfMetallicRoughness : GtlfMiscObject + { + public Vector4 BaseColorFactor { get; set; } = new Vector4(1,1,1,1); + public GltfMaterialTextureInfo? BaseColorTexture { get; set; } = null; + public float Metallic { get; set; } = 1; + public float Roughness { get; set; } = 1; + public GltfMaterialTextureInfo? MetallicRoughnessTexture { get; set; } = null; + + internal GltfMetallicRoughness(GltfDocument document) : base(document) + { + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected an object."); + + JsonElement child; + if (element.TryGetProperty("baseColorFactor", out child)) + { + if (child.ValueKind != JsonValueKind.Array) + { + throw new Exception("Expected a value array."); + } + + Vector4 color = Vector4.One; + int i = 0; + foreach (JsonElement item in child.EnumerateArray()) + { + float value = item.GetSingle(); + switch (i++) + { + case 0: color.X = value; break; + case 1: color.Y = value; break; + case 2: color.Z = value; break; + case 3: color.W = value; break; + default: + goto break_for; + } + + continue; + break_for: + break; + } + BaseColorFactor = color; + } + + if (element.TryGetProperty("baseColorTexture", out child)) + { + BaseColorTexture = new GltfMaterialTextureInfo(Document); + BaseColorTexture.Deserialize(child); + } + + if (element.TryGetProperty("metallicFactor", out child)) + { + Metallic = child.GetSingle(); + } + + if (element.TryGetProperty("roughnessFactor", out child)) + { + Roughness = child.GetSingle(); + } + + if (element.TryGetProperty("metallicRoughnessTexture", out child)) + { + MetallicRoughnessTexture = new GltfMaterialTextureInfo(Document); + MetallicRoughnessTexture.Deserialize(child); + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } + + + public class GltfNormalTexture : GltfMaterialTextureInfo + { + public float Scale { get; set; } = 1.0f; + + internal GltfNormalTexture(GltfDocument document) : base(document) + { + } + + internal override void Deserialize(JsonElement element) + { + base.Deserialize(element); + + if (element.TryGetProperty("scale", out JsonElement scale)) + { + Scale = scale.GetSingle(); + } + } + } + + public class GltfOcclusionTexture : GltfMaterialTextureInfo + { + public float Strength { get; set; } = 1; + + internal GltfOcclusionTexture(GltfDocument document) : base(document) + { + } + + internal override void Deserialize(JsonElement element) + { + base.Deserialize(element); + + if (element.TryGetProperty("strength", out JsonElement strength)) + { + Strength = strength.GetSingle(); + } + } + } + + public class GltfMaterial : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.Material; + private GltfMaterialAlphaMode _alphaMode = GltfMaterialAlphaMode.Opaque; + private string _alphaModeString = "OPAQUE"; + + public string? Name { get; set; } + + public string AlphaModeString + { + get => _alphaModeString; + set + { + _alphaModeString = value; + _alphaMode = GltfMaterialAlphaModeFromString(value); + } + } + + public GltfMaterialAlphaMode AlphaMode + { + get => _alphaMode; + set + { + _alphaMode = value; + _alphaModeString = GltfMaterialAlphaModeToString(value); + } + } + + public float AlphaCutoff { get; set; } = 0.5f; + + public bool DoubleSided { get; set; } = false; + + public GltfMetallicRoughness? MetallicRoughness { get; set; } = null; + public GltfNormalTexture? NormalTexture { get; set; } = null; + public GltfOcclusionTexture? OcclusionTexture { get; set; } = null; + public GltfMaterialTextureInfo? EmissiveTexture { get; set; } = null; + public Vector3 EmissiveFactor { get; set; } = Vector3.Zero; + + internal GltfMaterial(GltfDocument document) : base(document) { } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected an object."); + + DeserializeCommon(element); + + foreach (JsonProperty property in element.EnumerateObject()) + { + switch (property.Name) + { + case "name": + Name = property.Value.GetString(); + break; + case "pbrMetallicRoughness": + MetallicRoughness = new GltfMetallicRoughness(Document); + MetallicRoughness.Deserialize(property.Value); + break; + case "normalTexture": + NormalTexture = new GltfNormalTexture(Document); + NormalTexture.Deserialize(property.Value); + break; + case "occlusionTexture": + OcclusionTexture = new GltfOcclusionTexture(Document); + OcclusionTexture.Deserialize(property.Value); + break; + case "emissiveTexture": + EmissiveTexture = new GltfMaterialTextureInfo(Document); + EmissiveTexture.Deserialize(property.Value); + break; + case "emissiveFactor": + if (property.Value.ValueKind != JsonValueKind.Array) + { + throw new Exception("Expected a value array as emissiveFactor"); + } + + Vector3 factor = default; + int i = 0; + foreach (JsonElement item in property.Value.EnumerateArray()) + { + float value = item.GetSingle(); + switch (i++) + { + case 0: factor.X = value; break; + case 1: factor.Y = value; break; + case 2: factor.Z = value; break; + default: + goto break_for; + } + + continue; + break_for: + break; + } + break; + case "alphaMode": + AlphaModeString = property.Value.GetString() ?? "OPAQUE"; + break; + case "alphaCutoff": + AlphaCutoff = property.Value.GetSingle(); + break; + case "doubleSided": + DoubleSided = property.Value.GetBoolean(); + break; + } + } + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + + private static string GltfMaterialAlphaModeToString(GltfMaterialAlphaMode mode) => mode switch { + GltfMaterialAlphaMode.Opaque => "OPAQUE", + GltfMaterialAlphaMode.Mask => "MASK", + GltfMaterialAlphaMode.Blend => "BLEND", + _ => throw new ArgumentException("GltfMaterialAlphaMode.Other is not a valid target.", nameof(mode)), + }; + + private static GltfMaterialAlphaMode GltfMaterialAlphaModeFromString(string str) => str switch { + "OPAQUE" => GltfMaterialAlphaMode.Opaque, + "MASK" => GltfMaterialAlphaMode.Mask, + "BLEND" => GltfMaterialAlphaMode.Blend, + _ => GltfMaterialAlphaMode.Other + }; + } +} diff --git a/ReFuel.Gltf/GltfMesh.cs b/ReFuel.Gltf/GltfMesh.cs new file mode 100644 index 0000000..9fed229 --- /dev/null +++ b/ReFuel.Gltf/GltfMesh.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public record struct GltfMeshAttribute + { + private GltfSmartId accessor; + + public GltfAccessor Accessor + { + get => accessor.Value; + set => accessor.Value = value; + } + + public string Name { get; } + public int Index { get; } + public GltfAttributeType Type { get; } + + public GltfMeshAttribute(GltfDocument document, string name, int accessorIndex) + { + accessor = new GltfSmartId(document.Accessors) + { + Id = accessorIndex, + }; + + ReadOnlySpan full = name.AsSpan(); + ReadOnlySpan head; + ReadOnlySpan tail; + + int indexOfUnderscore = name.IndexOf('_'); + if (indexOfUnderscore == -1) + { + head = full; + tail = ReadOnlySpan.Empty; + } + else + { + head = full.Slice(0, indexOfUnderscore); + tail = full.Slice(indexOfUnderscore+1); + } + + switch (head.ToString()) + { + case "POSITION": + Name = "POSITION"; + Index = 0; + Type = GltfAttributeType.Position; + break; + case "NORMAL": + Name = "NORMAL"; + Index = 0; + Type = GltfAttributeType.Normal; + break; + case "TANGENT": + Name = "TANGENT"; + Index = 0; + Type = GltfAttributeType.Tangent; + break; + case "TEXCOORD": + Name = "TEXCOORD"; + Index = int.Parse(tail); + Type = GltfAttributeType.TexCoord; + break; + case "COLOR": + Name = "COLOR"; + Index = int.Parse(tail); + Type = GltfAttributeType.Color; + break; + case "JOINTS": + Name = "JOINTS"; + Index = int.Parse(tail); + Type = GltfAttributeType.Joints; + break; + case "WEIGHTS": + Name = "WEIGHTS"; + Index = int.Parse(tail); + Type = GltfAttributeType.Weights; + break; + default: + Name = tail.ToString(); + Index = 0; + Type = GltfAttributeType.Custom; + break; + } + } + } + + public class GltfMeshPrimitive : GltfObject + { + public override GltfObjectKind Kind => GltfObjectKind.MeshPrimitive; + private GltfSmartId index; + private GltfSmartId material; + + public List Attributes { get; } = new List(); + + public GltfAccessor? Index + { + get => index.IsSet ? index : null; + set + { + if (value is null) + index.Reset(); + else + index.Value = value; + } + } + + public GltfMaterial? Material + { + get => material.IsSet ? material : null; + set + { + if (value is null) + material.Reset(); + else + material.Value = value; + } + } + + public GltfMeshMode Mode { get; set; } = GltfMeshMode.Triangles; + + public GltfRefList Targets { get; private set; } + + internal GltfMeshPrimitive(GltfDocument document) : base(document) + { + Targets = new GltfRefList(null!); + index = new GltfSmartId(document.Accessors); + material = new GltfSmartId(document.Materials); + } + + internal override void Deserialize(JsonElement element) + { + AssertObject(element); + + if (!element.TryGetProperty("attributes", out JsonElement attributesElement)) + RequiredProperty("attributes"); + else if (attributesElement.ValueKind != JsonValueKind.Object) + PropertyFormatFail("attributes", "expected a dictionary."); + + foreach (JsonProperty property in attributesElement.EnumerateObject()) + { + if (!property.Value.TryGetInt32(out int i)) + { + PropertyFormatFail("attributes", "expected dictionary values to be integers."); + } + + Attributes.Add(new GltfMeshAttribute(Document, property.Name, i)); + } + + if (element.TryGetProperty("indices", out JsonElement indicesElement)) + { + if (!indicesElement.TryGetInt32(out int index)) + PropertyFormatFail("indices", "expected an integer value."); + + this.index.Id = index; + } + + if (element.TryGetProperty("material", out JsonElement materialElement)) + { + if (!materialElement.TryGetInt32(out int index)) + PropertyFormatFail("material", "expected an integer value."); + + this.material.Id = index; + } + + // TODO: materials. + // TODO: morph targets. + + if (element.TryGetProperty("mode", out JsonElement modeElement)) + { + if (!modeElement.TryGetInt32(out int mode)) + PropertyFormatFail("mode", "expected integer value."); + + Mode = (GltfMeshMode)mode; + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } + + public class GltfMesh : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.Mesh; + + public string? Name { get; set; } + public float[]? Weights { get; set; } + public List Primitives { get; } = new List(); + + internal GltfMesh(GltfDocument document) : base(document) + { + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected an object."); + + if (element.TryGetProperty("name", out JsonElement nameElement)) + { + Name = nameElement.GetString(); + } + + if (element.TryGetProperty("weights", out JsonElement weightsArray)) + { + if (weightsArray.ValueKind != JsonValueKind.Array) + throw new Exception("Expected an array."); + + List values = new List(); + foreach (JsonElement item in weightsArray.EnumerateArray()) + { + if (!item.TryGetSingle(out float v)) + throw new Exception("expected a number"); + + values.Add(v); + } + + Weights = values.ToArray(); + } + else + { + Weights = null; + } + + if (element.TryGetProperty("primitives", out JsonElement primitivesElement)) + { + if (primitivesElement.ValueKind != JsonValueKind.Array) + PropertyFormatFail("primitives", "expected an array."); + + foreach (JsonElement child in primitivesElement.EnumerateArray()) + { + GltfMeshPrimitive primitive = new GltfMeshPrimitive(Document); + primitive.Deserialize(child); + Primitives.Add(primitive); + } + } + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/ReFuel.Gltf/GltfMeshMode.cs b/ReFuel.Gltf/GltfMeshMode.cs new file mode 100755 index 0000000..73e32c1 --- /dev/null +++ b/ReFuel.Gltf/GltfMeshMode.cs @@ -0,0 +1,13 @@ +namespace ReFuel.Gltf +{ + public enum GltfMeshMode + { + Points = 0, + Lines = 1, + LineLoop = 2, + LineStrip = 3, + Triangles = 4, + TriangleStrip = 5, + TriangleFan = 6 + } +} diff --git a/ReFuel.Gltf/GltfMiscObject.cs b/ReFuel.Gltf/GltfMiscObject.cs new file mode 100644 index 0000000..663d6eb --- /dev/null +++ b/ReFuel.Gltf/GltfMiscObject.cs @@ -0,0 +1,11 @@ +namespace ReFuel.Gltf +{ + public abstract class GtlfMiscObject : GltfObject + { + public override GltfObjectKind Kind => GltfObjectKind.Misc; + + protected GtlfMiscObject(GltfDocument document) : base(document) + { + } + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/GltfNode.cs b/ReFuel.Gltf/GltfNode.cs new file mode 100755 index 0000000..7c8a116 --- /dev/null +++ b/ReFuel.Gltf/GltfNode.cs @@ -0,0 +1,407 @@ + +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public class GltfNode : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.Node; + + private GltfSmartId camera; + private GltfSmartId skin; + private GltfSmartId mesh; + private float[]? weights = null; + private TransformMode transformMode = TransformMode.Matrix; + private TransformUnion transform = new TransformUnion() { Matrix = Matrix4x4.Identity }; + + public string? Name { get; set; } + public object? Camera { get; set; } + public GltfRefList Children { get; } + public object? Skin { get; set; } + + public Matrix4x4 Matrix + { + get => transformMode == TransformMode.Matrix ? transform.Matrix : transform.ToMatrix().Matrix; + set + { + transformMode = TransformMode.Matrix; + transform.Matrix = value; + } + } + + public Quaternion Rotation + { + get => transformMode == TransformMode.SRT ? transform.Rotation : transform.ToSRT().Rotation; + + set + { + if (transformMode != TransformMode.SRT) + { + transformMode = TransformMode.SRT; + transform = transform.ToSRT(); + } + + transform.Rotation = value; + } + } + + public Vector3 Scale + { + get => transformMode == TransformMode.SRT ? + transform.Scale : + transform.ToSRT().Scale; + set + { + if (transformMode != TransformMode.SRT) + { + transformMode = TransformMode.SRT; + transform = transform.ToSRT(); + } + + transform.Scale = value; + } + } + + public Vector3 Translation + { + get => transformMode == TransformMode.SRT ? + transform.Translation : + transform.ToSRT().Translation; + set + { + if (transformMode != TransformMode.SRT) + { + transformMode = TransformMode.SRT; + transform = transform.ToSRT(); + } + + transform.Translation = value; + } + } + + public GltfMesh? Mesh + { + get => mesh.IsSet ? mesh.Value : null; + set + { + if (value is null) + mesh.Reset(); + else + mesh.Value = value; + } + } + public float[]? Weights => weights; + + internal GltfNode(GltfDocument document) : base(document) + { + Children = new GltfRefList(document.Nodes); + mesh = new GltfSmartId(document.Meshes); + } + + /// + /// Optimize this node for matrix mode transforms. + /// + /// Self + public GltfNode MatrixMode() + { + if (transformMode == TransformMode.Matrix) + return this; + + transformMode = TransformMode.Matrix; + transform = transform.ToMatrix(); + return this; + } + + /// + /// Optimize this node for scale rotation translation transforms. + /// + /// Self + public GltfNode SrtMode() + { + if (transformMode == TransformMode.SRT) + return this; + + transformMode = TransformMode.SRT; + transform = transform.ToSRT(); + + return this; + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected node to be an object."); + + if (element.TryGetProperty("name", out JsonElement nameProperty)) + Name = nameProperty.ToString(); + + if (element.TryGetProperty("camera", out JsonElement cameraProperty)) + { + if (!cameraProperty.TryGetInt32(out int cameraId)) + throw new Exception("Expected camera id to be a number."); + + camera.Id = cameraId; + } + + if (element.TryGetProperty("skin", out JsonElement skinElement)) + { + if (!skinElement.TryGetInt32(out int skinId)) + throw new Exception("Expected skin id to be a number."); + + skin.Id = skinId; + } + + if (element.TryGetProperty("mesh", out JsonElement meshElement)) + { + if (!meshElement.TryGetInt32(out int meshId)) + throw new Exception("Expected mesh id to be a number."); + + mesh.Id = meshId; + } + + if (element.TryGetProperty("children", out JsonElement childrenElement)) + { + if (childrenElement.ValueKind != JsonValueKind.Array) + throw new Exception("Expected a JSON array for children."); + + foreach (JsonElement child in childrenElement.EnumerateArray()) + { + if (!child.TryGetInt32(out int id)) + throw new Exception("Expected a number for child id."); + + Children.Add(id); + } + } + + if (element.TryGetProperty("matrix", out JsonElement matrixElement)) + { + if (matrixElement.ValueKind != JsonValueKind.Array) + throw new Exception("Expected matrix to be an array."); + + var enumerator = matrixElement.EnumerateArray(); + + transformMode = TransformMode.Matrix; + transform.Matrix = Matrix4x4.Identity; + + for (int x = 0; x < 4; x++) + for (int y = 0; y < 4 && enumerator.MoveNext(); y++) + { + if (!enumerator.Current.TryGetSingle(out float v)) + throw new Exception("Expected a floating point number as matrix elements."); + StoreMat(ref transform.Matrix, x, y, v); + } + } + else + { + if (element.TryGetProperty("scale", out JsonElement scaleElement)) + { + if (scaleElement.ValueKind != JsonValueKind.Array) + throw new Exception("Expected scale to be an array."); + + transformMode = TransformMode.SRT; + Vector3 scale = default; + var enumerator = scaleElement.EnumerateArray(); + + for (int i = 0; i < 3 && enumerator.MoveNext(); i++) + { + if (!enumerator.Current.TryGetSingle(out float v)) + throw new Exception("Expected a floating point number as scale elements."); + StoreVec(ref scale, i, v); + } + + Scale = scale; + } + + if (element.TryGetProperty("rotation", out JsonElement rotationElement)) + { + if (scaleElement.ValueKind != JsonValueKind.Array) + throw new Exception("Expected rotation to be an array."); + + transformMode = TransformMode.SRT; + Quaternion rotation = Quaternion.Identity; + var enumerator = scaleElement.EnumerateArray(); + + for (int i = 0; i < 4 && enumerator.MoveNext(); i++) + { + if (!enumerator.Current.TryGetSingle(out float v)) + throw new Exception("Expected a floating point number as rotation elements."); + StoreQuat(ref rotation, i, v); + } + + Rotation = rotation; + } + + if (element.TryGetProperty("translation", out JsonElement translationElement)) + { + if (scaleElement.ValueKind != JsonValueKind.Array) + throw new Exception("Expected translation to be an array."); + + transformMode = TransformMode.SRT; + Vector3 translation = default; + var enumerator = scaleElement.EnumerateArray(); + + for (int i = 0; i < 3 && enumerator.MoveNext(); i++) + { + if (!enumerator.Current.TryGetSingle(out float v)) + throw new Exception("Expected a floating point number as rotation elements."); + StoreVec(ref translation, i, v); + } + + Translation = translation; + } + } + + DeserializeCommon(element); + + static void StoreMat(ref Matrix4x4 m, int r, int c, float x) + { + // I love System.Numerics... + switch (r, c) + { + case (0, 0): m.M11 = x; break; + case (0, 1): m.M12 = x; break; + case (0, 2): m.M13 = x; break; + case (0, 3): m.M14 = x; break; + + case (1, 0): m.M21 = x; break; + case (1, 1): m.M22 = x; break; + case (1, 2): m.M23 = x; break; + case (1, 3): m.M24 = x; break; + + case (2, 0): m.M31 = x; break; + case (2, 1): m.M32 = x; break; + case (2, 2): m.M33 = x; break; + case (2, 3): m.M34 = x; break; + + case (3, 0): m.M41 = x; break; + case (3, 1): m.M42 = x; break; + case (3, 2): m.M43 = x; break; + case (3, 3): m.M44 = x; break; + }; + } + + static void StoreVec(ref Vector3 v, int r, float x) + { + switch (r) + { + case 0: v.X = x; break; + case 1: v.Y = x; break; + case 2: v.Z = x; break; + } + } + + static void StoreQuat(ref Quaternion q, int r, float x) + { + switch (r) + { + case 0: q.X = x; break; + case 1: q.Y = x; break; + case 2: q.Z = x; break; + case 3: q.W = x; break; + } + } + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + + [StructLayout(LayoutKind.Explicit)] + private struct TransformUnion + { + [FieldOffset(0 * sizeof(float))] public Matrix4x4 Matrix; + + [FieldOffset(0 * sizeof(float))] public Vector3 Scale; + [FieldOffset(4 * sizeof(float))] public Quaternion Rotation; + [FieldOffset(8 * sizeof(float))] public Vector3 Translation; + + public TransformUnion ToSRT() + { + Vector3 r0 = new Vector3(Matrix.M11, Matrix.M12, Matrix.M13); + Vector3 r1 = new Vector3(Matrix.M11, Matrix.M12, Matrix.M13); + Vector3 r2 = new Vector3(Matrix.M11, Matrix.M12, Matrix.M13); + + float r0l = r0.Length(); + float r1l = r1.Length(); + float r2l = r2.Length(); + + Vector3 scale = new Vector3(r0l, r1l, r2l); + Vector3 translation = new Vector3(Matrix.M41, Matrix.M42, Matrix.M43); + + // The following code segment has been adapted from OpenTK.Mathematics (MIT), which adapted from + // Blender (GPLv2 or more permissive). + // However, it is probably not copyrighted considering it is math. + + r0 /= r0l; + r1 /= r1l; + r2 /= r2l; + + Quaternion rotation = default; + float trace = 0.25f * (r0.X + r1.Y + r2.Z + 1f); + + if (trace > 0) + { + float sq = MathF.Sqrt(trace); + + rotation.W = sq; + sq = 1.0f / (4.0f * sq); + rotation.X = (r1.Z - r2.Y) * sq; + rotation.Y = (r2.X - r0.Z) * sq; + rotation.Z = (r0.Y - r1.X) * sq; + } + else if (r0.X > r1.Y && r0.X > r2.Z) + { + float sq = 2.0f * MathF.Sqrt(1.0f + r0.X - r1.Y - r2.Z); + + rotation.X = 0.25f * sq; + sq = 1.0f / sq; + rotation.W = (r2.Y - r1.Z) * sq; + rotation.Y = (r1.X + r0.Y) * sq; + rotation.Z = (r2.X + r0.Z) * sq; + } + else if (r1.Y > r2.Z) + { + float sq = 2.0f * MathF.Sqrt(1.0f + r1.Y - r0.X - r2.Z); + + rotation.Y = 0.25f * sq; + sq = 1.0f / sq; + rotation.W = (r2.X - r0.Z) * sq; + rotation.X = (r1.X + r0.Y) * sq; + rotation.Z = (r2.Y + r1.Z) * sq; + } + else + { + float sq = 2.0f * MathF.Sqrt(1.0f + r2.Z - r0.X - r1.Y); + + rotation.Z = (float)(0.25 * sq); + sq = 1.0f / sq; + rotation.W = (float)((r1.X - r0.Y) * sq); + rotation.X = (float)((r2.X + r0.Z) * sq); + rotation.Y = (float)((r2.Y + r1.Z) * sq); + } + + rotation = Quaternion.Normalize(rotation); + + return new TransformUnion() { Scale = scale, Rotation = rotation, Translation = translation }; + } + + public TransformUnion ToMatrix() + { + Matrix4x4 matrix = Matrix4x4.CreateScale(Scale) * + Matrix4x4.CreateFromQuaternion(Rotation) * + Matrix4x4.CreateTranslation(Translation); + + return new TransformUnion() { Matrix = matrix }; + } + } + + private enum TransformMode + { + Matrix, + SRT + } + } +} diff --git a/ReFuel.Gltf/GltfObject.cs b/ReFuel.Gltf/GltfObject.cs new file mode 100755 index 0000000..316f399 --- /dev/null +++ b/ReFuel.Gltf/GltfObject.cs @@ -0,0 +1,86 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public enum GltfObjectKind + { + Document, + Asset, + Scene, + Node, + Camera, + Mesh, + Buffer, + BufferView, + Accessor, + Material, + Texture, + Image, + Sampler, + Skin, + Animation, + MeshPrimitive, + Misc, + } + + public abstract class GltfObject + { + public abstract GltfObjectKind Kind { get; } + public GltfDocument Document { get; protected set; } + public GltfCollection Extension { get; protected set; } = new GltfCollection(); + public GltfCollection Extras { get; protected set; } = new GltfCollection(); + + protected GltfObject() + { + Unsafe.SkipInit(out GltfDocument doc); + Document = doc; + } + + protected GltfObject(GltfDocument document) : this() + { + Document = document; + } + + protected void DeserializeCommon(JsonElement element) + { + if (element.TryGetProperty("extras", out JsonElement extras)) + { + Extras.Parse(extras); + } + if (element.TryGetProperty("extension", out JsonElement extension)) + { + Extension.Parse(extension); + } + } + + internal abstract void Deserialize(JsonElement element); + internal abstract void Serialize(Utf8JsonWriter writer); + + protected void AssertObject(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception($"Gltf document node '{Kind}' expects an object in the JSON."); + } + + protected void AssertArray(JsonElement element, string property) + { + if (element.ValueKind != JsonValueKind.Array) + throw new Exception($"Gltf document '{property}' expects an array in the JSON."); + } + + protected void RequiredProperty(string name) + { + throw new Exception($"Gltf document node '{Kind}' requries property '{name}'."); + } + + protected void PropertyFormatFail(string name, string? message) + { + if (message != null) + throw new Exception($"Gltf document node '{Kind}', property '{name}' has invalid format: {message}"); + else + throw new Exception($"Gltf document node '{Kind}', property '{name}' has invalid format."); + } + } +} diff --git a/ReFuel.Gltf/GltfPrimitiveTopology.cs b/ReFuel.Gltf/GltfPrimitiveTopology.cs new file mode 100755 index 0000000..ece63b7 --- /dev/null +++ b/ReFuel.Gltf/GltfPrimitiveTopology.cs @@ -0,0 +1,13 @@ +namespace ReFuel.Gltf +{ + public enum GltfPrimitiveTopology + { + Points = 0, + Lines = 1, + LineLoop = 2, + LineStrip = 3, + Triangles = 4, + TriangleStrip = 5, + TriangleFan = 6 + } +} diff --git a/ReFuel.Gltf/GltfSampler.cs b/ReFuel.Gltf/GltfSampler.cs new file mode 100644 index 0000000..ebd35bc --- /dev/null +++ b/ReFuel.Gltf/GltfSampler.cs @@ -0,0 +1,93 @@ +using System; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + internal enum GltfSamplerValue : int + { + Nearest = 9728, + Linear = 9729, + NearestMipmapNearest = 9984, + LinearMipmapNearest = 9985, + NearestMipmapLinear = 9986, + LinearMipmapLinear = 9987, + ClampToEdge = 33071, + MirroredRepeat = 33648, + Repeat = 10497, + } + + public enum GltfSamplerMagFilter + { + Unset = 0, + Nearest = GltfSamplerValue.Nearest, + Linear = GltfSamplerValue.Linear + } + + public enum GltfSamplerMinFilter + { + Unset = 0, + Nearest = GltfSamplerValue.Nearest, + Linear = GltfSamplerValue.Linear, + NearestMipmapNearest = GltfSamplerValue.NearestMipmapNearest, + LinearMipmapNearest = GltfSamplerValue.LinearMipmapNearest, + NearestMipmapLinear = GltfSamplerValue.NearestMipmapLinear, + LinearMipmapLinear = GltfSamplerValue.LinearMipmapLinear, + } + + public enum GltfSamplerWrapMode + { + ClampToEdge = GltfSamplerValue.ClampToEdge, + MirroredRepeat = GltfSamplerValue.MirroredRepeat, + Repeat = GltfSamplerValue.Repeat, + } + + public class GltfSampler : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.Sampler; + + public string? Name { get; set; } = null; + public GltfSamplerMinFilter MinFilter { get; set; } = GltfSamplerMinFilter.Unset; + public GltfSamplerMagFilter MagFilter { get; set; } = GltfSamplerMagFilter.Unset; + public GltfSamplerWrapMode WrapS { get; set; } = GltfSamplerWrapMode.Repeat; + public GltfSamplerWrapMode WrapT { get; set; } = GltfSamplerWrapMode.Repeat; + + internal GltfSampler(GltfDocument document) : base(document) + { + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected an object as a sampler."); + + foreach (JsonProperty property in element.EnumerateObject()) + { + switch (property.Name) + { + case "name": + Name = property.Value.GetString(); + break; + case "magFilter": + MagFilter = (GltfSamplerMagFilter)property.Value.GetInt32(); + break; + case "minFilter": + MinFilter = (GltfSamplerMinFilter)property.Value.GetInt32(); + break; + case "wrapS": + WrapS = (GltfSamplerWrapMode)property.Value.GetInt32(); + break; + case "wrapT": + WrapT = (GltfSamplerWrapMode)property.Value.GetInt32(); + break; + } + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/GltfScene.cs b/ReFuel.Gltf/GltfScene.cs new file mode 100755 index 0000000..82cecc3 --- /dev/null +++ b/ReFuel.Gltf/GltfScene.cs @@ -0,0 +1,49 @@ +using System; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public class GltfScene : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.Scene; + public string? Name { get; set; } + public GltfRefList Nodes { get; } + + internal GltfScene(GltfDocument document) : base(document) + { + Nodes = new GltfRefList(document.Nodes); + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected an object as an asset."); + + JsonElement value; + + if (element.TryGetProperty("name", out value)) + Name = value.GetString(); + + if (element.TryGetProperty("nodes", out value)) + { + if (value.ValueKind != JsonValueKind.Array) + throw new Exception("Expected nodes to be an integer array."); + + foreach (JsonElement node in value.EnumerateArray()) + { + if (!node.TryGetInt32(out int id)) + throw new Exception("Expected a node ID in nodes array."); + + Nodes.Add(id); + } + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/ReFuel.Gltf/GltfSerializedExtensionProvider.cs b/ReFuel.Gltf/GltfSerializedExtensionProvider.cs new file mode 100755 index 0000000..0bd8786 --- /dev/null +++ b/ReFuel.Gltf/GltfSerializedExtensionProvider.cs @@ -0,0 +1,45 @@ +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ReFuel.Gltf +{ + public class GltfSerializedExtensionProvider : IGltfExtensionProvider + { + public string Name { get; } + + public Type? DataType { get; } = typeof(T); + + public GltfSerializedExtensionProvider(string name) + { + Name = name; + if (typeof(T).GetCustomAttribute() is null) + throw new Exception("This type is not JSON serializable."); + } + + public T ParseExtension(JsonElement element) + { + return JsonSerializer.Deserialize(element) ?? throw new Exception("Could not deserialize extension object."); + } + + public object ParseExtensionObject(JsonElement element) + { + return ParseExtension(element)!; + } + + public void WriteExtension(T value, Utf8JsonWriter writer) + { + JsonSerializer.Serialize(writer, value); + } + + public void WriteExtensionObject(object obj, Utf8JsonWriter writer) + { + if (obj is T extensionType) + { + WriteExtension(extensionType, writer); + } + else throw new Exception($"Unexpected type of object {obj.GetType()}."); + } + } +} diff --git a/ReFuel.Gltf/GltfSkin.cs b/ReFuel.Gltf/GltfSkin.cs new file mode 100644 index 0000000..c717e1c --- /dev/null +++ b/ReFuel.Gltf/GltfSkin.cs @@ -0,0 +1,81 @@ +using System; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public class GltfSkin : GltfIdObject + { + private GltfSmartId _inverseBindMatrices = new GltfSmartId(); + private GltfSmartId _skeleton = new GltfSmartId(); + + public override GltfObjectKind Kind => GltfObjectKind.Skin; + + public string? Name { get; set; } + public GltfAccessor? InverseBindMatrices + { + get => _inverseBindMatrices.IsSet ? _inverseBindMatrices.Value : null; + set + { + if (value is null) + _inverseBindMatrices.Reset(); + else + _inverseBindMatrices.Value = value; + } + } + public GltfNode? Skeleton + { + get => _skeleton.IsSet ? _skeleton.Value : null; + set + { + if (value is null) + _skeleton.Reset(); + else + _skeleton.Value = value; + } + } + + public GltfRefList Joints { get; } + + internal GltfSkin(GltfDocument document) : base(document) + { + Joints = new GltfRefList(document.Nodes); + } + + internal override void Deserialize(JsonElement element) + { + AssertObject(element); + + if (!element.TryGetProperty("joints", out JsonElement jointsElement)) + RequiredProperty("joints"); + AssertArray(jointsElement, "joints"); + + foreach (JsonElement joint in jointsElement.EnumerateArray()) + { + Joints.Add(joint.GetInt32()); + } + + foreach (JsonProperty property in element.EnumerateObject()) + { + switch (property.Name) + { + case "inverseBindMatrices": + _inverseBindMatrices.Id = property.Value.GetInt32(); + break; + case "skeleton": + _skeleton.Id = property.Value.GetInt32(); + break; + case "name": + Name = property.Value.GetString(); + break; + } + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/GltfTargetHint.cs b/ReFuel.Gltf/GltfTargetHint.cs new file mode 100755 index 0000000..57eacc3 --- /dev/null +++ b/ReFuel.Gltf/GltfTargetHint.cs @@ -0,0 +1,9 @@ +namespace ReFuel.Gltf +{ + public enum GltfTargetHint + { + None, + ArrayBuffer = 34962, + ElementArrayBuffer = 3463 + } +} diff --git a/ReFuel.Gltf/GltfTexture.cs b/ReFuel.Gltf/GltfTexture.cs new file mode 100644 index 0000000..3169014 --- /dev/null +++ b/ReFuel.Gltf/GltfTexture.cs @@ -0,0 +1,74 @@ +using System; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public class GltfTexture : GltfIdObject + { + public override GltfObjectKind Kind => GltfObjectKind.Texture; + private GltfSmartId _sampler; + private GltfSmartId _source; + + public string? Name { get; set; } = null; + + public GltfSampler? Sampler + { + get => _sampler.IsSet ? _sampler.Value : null; + set + { + if (value is null) + _sampler.Reset(); + else + _sampler.Value = value; + } + } + + public GltfImage? Source + { + get => _source.IsSet ? _source.Value : null; + set + { + if (value is null) + _source.Reset(); + else + _source.Value = value; + } + } + + + internal GltfTexture(GltfDocument document) : base(document) + { + _sampler = new GltfSmartId(document.Samplers); + _source = new GltfSmartId(document.Images); + } + + internal override void Deserialize(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + throw new Exception("Expected an object."); + + foreach (JsonProperty property in element.EnumerateObject()) + { + switch (property.Name) + { + case "name": + Name = property.Value.GetString(); + break; + case "source": + _source.Id = property.Value.GetInt32(); + break; + case "sampler": + _sampler.Id = property.Value.GetInt32(); + break; + } + } + + DeserializeCommon(element); + } + + internal override void Serialize(Utf8JsonWriter writer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/IGltfExtensionProvider.cs b/ReFuel.Gltf/IGltfExtensionProvider.cs new file mode 100755 index 0000000..3aa0dad --- /dev/null +++ b/ReFuel.Gltf/IGltfExtensionProvider.cs @@ -0,0 +1,20 @@ +using System; +using System.Text.Json; + +namespace ReFuel.Gltf +{ + public interface IGltfExtensionProvider + { + string Name { get; } + Type? DataType { get; } + + object ParseExtensionObject(JsonElement element); + void WriteExtensionObject(object obj, Utf8JsonWriter writer); + } + + public interface IGltfExtensionProvider : IGltfExtensionProvider + { + T ParseExtension(JsonElement element); + void WriteExtension(T value, Utf8JsonWriter writer); + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/IGltfStreamProvider.cs b/ReFuel.Gltf/IGltfStreamProvider.cs new file mode 100644 index 0000000..1498ff0 --- /dev/null +++ b/ReFuel.Gltf/IGltfStreamProvider.cs @@ -0,0 +1,18 @@ +using System; +using System.IO; + +namespace ReFuel.Gltf +{ + /// + /// Provides users with special stream opening logic. + /// + public interface IGltfStreamProvider + { + /// + /// Open the file at the given URI. + /// + /// The present working directory of the file. + /// The URI of the file to open + public Stream? OpenFile(string? pwd, Uri uri); + } +} \ No newline at end of file diff --git a/ReFuel.Gltf/IO/GltfUriReader.cs b/ReFuel.Gltf/IO/GltfUriReader.cs new file mode 100755 index 0000000..66fa7f5 --- /dev/null +++ b/ReFuel.Gltf/IO/GltfUriReader.cs @@ -0,0 +1,62 @@ +namespace ReFuel.Gltf.IO +{ + public class GltfUriReader : Stream + { + public Uri Uri { get; } + public string Type { get; } + public string Encoding { get; } + + private string data; + private int index = 0; + + public GltfUriReader(Uri uri) + { + if (uri.Scheme != "data") + throw new Exception("Expected a URI with 'data:' scheme."); + + Uri = uri; + data = uri.AbsolutePath; + index = data.IndexOf(',') + 1; + + if (index == 0) + { + throw new Exception("No data string."); + } + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => throw new NotImplementedException(); + + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/ReFuel.Gltf/IO/StreamWrapper.cs b/ReFuel.Gltf/IO/StreamWrapper.cs new file mode 100644 index 0000000..7c08158 --- /dev/null +++ b/ReFuel.Gltf/IO/StreamWrapper.cs @@ -0,0 +1,98 @@ +namespace ReFuel.Gltf.IO +{ + public class StreamWrapper : Stream + { + private readonly Stream _proxy; + + public Stream BaseStream { get; } + public bool KeepOpen { get; } + public long Offset { get; private init; } = 0; + public long Limit { get; private init; } = -1; + + public override bool CanRead => _proxy.CanRead; + public override bool CanSeek => _proxy.CanSeek; + public override bool CanWrite => _proxy.CanWrite; + public override long Length => Limit < 0 ? _proxy.Length : Math.Min(Limit, Length - Offset); + + public override long Position + { + get => ToRelOffset(_proxy.Position); + set => _proxy.Position = ToAbsOffset(value); + } + + public StreamWrapper(Stream baseStream, bool keepOpen = false) + { + BaseStream = baseStream; + KeepOpen = keepOpen; + + _proxy = Synchronized(baseStream); + } + + public override void Flush() => _proxy.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => _proxy.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => _proxy.Seek(offset, origin); + + public override void SetLength(long value) => _proxy.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => _proxy.Write(buffer, offset, count); + + public StreamWrapper Fork(long offset = 0, long limit = -1, bool keepOpen = false) + { + if (limit < 0) + limit = Limit; + + if (offset < 0 || offset > Length) + throw new ArgumentOutOfRangeException(nameof(offset), "Offset out of range."); + + if (limit >= 0 && offset + limit > Length) + throw new ArgumentOutOfRangeException(nameof(limit), "Limit too large for the given offset."); + + return ForkAbs(ToAbsOffset(offset), limit, keepOpen); + } + + public StreamWrapper ForkAbs(long offset = 0, long limit = -1, bool keepOpen = false) + { + if (offset < 0 || offset > BaseStream.Length) + throw new ArgumentOutOfRangeException(nameof(offset), "Offset out of range."); + + if (limit >= 0 && offset + limit > BaseStream.Length) + throw new ArgumentOutOfRangeException(nameof(limit), "Limit too large for the given offset."); + + return new StreamWrapper(BaseStream, keepOpen) + { + Offset = offset, + Limit = limit, + }; + } + + private long ToAbsOffset(long offset) + { + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), offset, "The relative offset cannot be less than zero."); + + if (Limit >= 0 && offset > Limit) + throw new ArgumentOutOfRangeException(nameof(offset), offset, "The relative offset cannot be greater than the limit."); + + return offset + Offset; + } + + private long ToRelOffset(long offset) + { + return offset - Offset; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) + return; + + _proxy.Dispose(); + if (!KeepOpen) + BaseStream.Dispose(); + } + } +} diff --git a/ReFuel.Gltf/ReFuel.Gltf.csproj b/ReFuel.Gltf/ReFuel.Gltf.csproj new file mode 100644 index 0000000..ad9ab37 --- /dev/null +++ b/ReFuel.Gltf/ReFuel.Gltf.csproj @@ -0,0 +1,10 @@ + + + + net6.0;net8.0 + enable + enable + 0.0.1 + true + +