From f1c1ae7ebbdb6c990bde6005d65e602b64851d42 Mon Sep 17 00:00:00 2001 From: DomCR Date: Mon, 2 Feb 2026 15:52:22 +0100 Subject: [PATCH 1/4] json centralized --- src/MeshIO/Formats/Gltf/JsonUtils.cs | 9 +++++++++ src/MeshIO/Formats/Gltf/Schema/GlbHeader.cs | 4 ++-- src/MeshIO/Formats/Gltf/Schema/V2/GltfRoot.cs | 10 +++++----- 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 src/MeshIO/Formats/Gltf/JsonUtils.cs diff --git a/src/MeshIO/Formats/Gltf/JsonUtils.cs b/src/MeshIO/Formats/Gltf/JsonUtils.cs new file mode 100644 index 0000000..3edd057 --- /dev/null +++ b/src/MeshIO/Formats/Gltf/JsonUtils.cs @@ -0,0 +1,9 @@ +namespace MeshIO.Formats.Gltf; + +internal static class JsonUtils +{ + public static T Deserialize(string json) + { + return Newtonsoft.Json.JsonConvert.DeserializeObject(json); + } +} diff --git a/src/MeshIO/Formats/Gltf/Schema/GlbHeader.cs b/src/MeshIO/Formats/Gltf/Schema/GlbHeader.cs index 4e22e68..2cc78fc 100644 --- a/src/MeshIO/Formats/Gltf/Schema/GlbHeader.cs +++ b/src/MeshIO/Formats/Gltf/Schema/GlbHeader.cs @@ -53,14 +53,14 @@ public GltfRoot GetRoot() if (this.Version == GltfVersion.V1) { string json = Encoding.UTF8.GetString(JsonData); - var map = Newtonsoft.Json.JsonConvert.DeserializeObject>(json); + var map = JsonUtils.Deserialize>(json); return new GltfRoot(map); } else { string json = Encoding.UTF8.GetString(JsonData); - return Newtonsoft.Json.JsonConvert.DeserializeObject(json); + return JsonUtils.Deserialize(json); } } diff --git a/src/MeshIO/Formats/Gltf/Schema/V2/GltfRoot.cs b/src/MeshIO/Formats/Gltf/Schema/V2/GltfRoot.cs index 66f6420..c684eda 100644 --- a/src/MeshIO/Formats/Gltf/Schema/V2/GltfRoot.cs +++ b/src/MeshIO/Formats/Gltf/Schema/V2/GltfRoot.cs @@ -568,7 +568,7 @@ public GltfRoot(Dictionary map) //setMap(item.Value, ref this._animations); break; case "asset": - Asset = Newtonsoft.Json.JsonConvert.DeserializeObject(item.Value.ToString()); + Asset = JsonUtils.Deserialize(item.Value.ToString()); break; case "buffers": setMap(item.Value, ref this._buffers); @@ -583,7 +583,7 @@ public GltfRoot(Dictionary map) setMap(item.Value, ref this._textures); break; case "extensionsused": - ExtensionsUsed = Newtonsoft.Json.JsonConvert.DeserializeObject(item.Value.ToString()); + ExtensionsUsed = JsonUtils.Deserialize(item.Value.ToString()); break; case "images": setMap(item.Value, ref this._images); @@ -628,7 +628,7 @@ public GltfRoot(Dictionary map) public Dictionary Techniques { get; } = new(); private Dictionary getMap(string json) { - return Newtonsoft.Json.JsonConvert.DeserializeObject>(json); + return JsonUtils.Deserialize>(json); } private void setMap(object obj, Dictionary arr) @@ -638,7 +638,7 @@ private void setMap(object obj, Dictionary arr) { var name = item.Key.ToLower(); var json = item.Value.ToString(); - T t = Newtonsoft.Json.JsonConvert.DeserializeObject(item.Value.ToString()); + T t = JsonUtils.Deserialize(item.Value.ToString()); arr.Add(name, t); } } @@ -652,7 +652,7 @@ private void setMap(object obj, ref T[] arr) { var item = map.ElementAt(i); var json = item.Value.ToString(); - T entry = Newtonsoft.Json.JsonConvert.DeserializeObject(json); + T entry = JsonUtils.Deserialize(json); entry.Name = item.Key; arr[i] = entry; } From 36e04039facbad75e5479483defd8fb8b5b612a7 Mon Sep 17 00:00:00 2001 From: DomCR Date: Thu, 5 Feb 2026 11:16:09 +0100 Subject: [PATCH 2/4] first approach --- .../Formats/Gltf/GltfReaderLocalTest.cs | 74 ++++++++++++++++--- .../Formats/Gltf/GltfReaderTests.cs | 4 +- src/MeshIO/Formats/Gltf/GlbReader.cs | 6 +- src/MeshIO/Formats/Gltf/GltfReader.cs | 35 +++++++++ .../Formats/Gltf/Readers/GlbFileBuilder.cs | 6 +- .../Schema/{GlbHeader.cs => GltfHeader.cs} | 56 +++++++++----- src/MeshIO/Formats/Gltf/Schema/GltfVersion.cs | 1 + src/MeshIO/MeshIO.csproj | 1 - 8 files changed, 146 insertions(+), 37 deletions(-) create mode 100644 src/MeshIO/Formats/Gltf/GltfReader.cs rename src/MeshIO/Formats/Gltf/Schema/{GlbHeader.cs => GltfHeader.cs} (66%) diff --git a/src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs b/src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs index a6b1139..f28a2fe 100644 --- a/src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs +++ b/src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs @@ -10,9 +10,13 @@ namespace MeshIO.Tests.Formats.Gltf; public class GltfReaderLocalTest : IOTestsBase { - public static readonly TheoryData V1Files = new(); + public static readonly TheoryData GlbV1Files = new(); - public static readonly TheoryData V2Files = new(); + public static readonly TheoryData GlbV2Files = new(); + + public static readonly TheoryData GltfV1Files = new(); + + public static readonly TheoryData GltfV2Files = new(); private const string _samplesFolder = "..\\..\\..\\..\\..\\..\\glTF-Sample-Models"; @@ -20,27 +24,46 @@ static GltfReaderLocalTest() { if (!Directory.Exists(_samplesFolder)) { - V1Files.Add(new FileModel()); - V2Files.Add(new FileModel()); + GlbV1Files.Add(new FileModel()); + GlbV2Files.Add(new FileModel()); return; } foreach (string file in Directory.GetFiles(Path.Combine(_samplesFolder, "1.0"), "*.glb", SearchOption.AllDirectories)) { FileModel model = new FileModel(file); - V1Files.Add(model); + GlbV1Files.Add(model); + } + + foreach (string file in Directory.GetFiles(Path.Combine(_samplesFolder, "1.0"), "*.gltf", SearchOption.AllDirectories)) + { + if (file.Contains("glTF-Embedded")) + continue; + + FileModel model = new FileModel(file); + GltfV1Files.Add(model); } foreach (string file in Directory.GetFiles(Path.Combine(_samplesFolder, "2.0"), "*.glb", SearchOption.AllDirectories)) { - V2Files.Add(new FileModel(file)); + GlbV2Files.Add(new FileModel(file)); + } + + foreach (string file in Directory.GetFiles(Path.Combine(_samplesFolder, "2.0"), "*.gltf", SearchOption.AllDirectories)) + { + if (file.Contains("Draco") || file.Contains("glTF-Embedded")) + continue; + + GltfV2Files.Add(new FileModel(file)); } } - public GltfReaderLocalTest(ITestOutputHelper output) : base(output) { } + public GltfReaderLocalTest(ITestOutputHelper output) : base(output) + { + } [Theory] - [MemberData(nameof(V1Files))] + [MemberData(nameof(GlbV1Files))] public void ReadGlbV1(FileModel test) { if (string.IsNullOrEmpty(test.Path)) @@ -57,7 +80,7 @@ public void ReadGlbV1(FileModel test) } [Theory] - [MemberData(nameof(V2Files))] + [MemberData(nameof(GlbV2Files))] public void ReadGlbV2(FileModel test) { if (string.IsNullOrEmpty(test.Path)) @@ -69,4 +92,35 @@ public void ReadGlbV2(FileModel test) reader.Read(); } } -} + + [Theory] + [MemberData(nameof(GltfV1Files))] + public void ReadGltfV1(FileModel test) + { + if (string.IsNullOrEmpty(test.Path)) + return; + + Scene scene = null; + using (GltfReader reader = new GltfReader(test.Path)) + { + reader.OnNotification += this.onNotification; + scene = reader.Read(); + } + + Assert.NotNull(scene); + } + + [Theory] + [MemberData(nameof(GltfV2Files))] + public void ReadGltfV2(FileModel test) + { + if (string.IsNullOrEmpty(test.Path)) + return; + + using (GltfReader reader = new GltfReader(test.Path)) + { + reader.OnNotification += this.onNotification; + reader.Read(); + } + } +} \ No newline at end of file diff --git a/src/MeshIO.Tests/Formats/Gltf/GltfReaderTests.cs b/src/MeshIO.Tests/Formats/Gltf/GltfReaderTests.cs index 19e74f0..90c8805 100644 --- a/src/MeshIO.Tests/Formats/Gltf/GltfReaderTests.cs +++ b/src/MeshIO.Tests/Formats/Gltf/GltfReaderTests.cs @@ -33,11 +33,11 @@ public void ReadGlb(FileModel test) } } - [Theory(Skip = "Gltf not implemented")] + [Theory] [MemberData(nameof(GltfFiles))] public void ReadGltf(FileModel test) { - using (GlbReader reader = new GlbReader(test.Path)) + using (GltfReader reader = new GltfReader(test.Path)) { reader.OnNotification += this.onNotification; reader.Read(); diff --git a/src/MeshIO/Formats/Gltf/GlbReader.cs b/src/MeshIO/Formats/Gltf/GlbReader.cs index 310e850..4bd6b6b 100644 --- a/src/MeshIO/Formats/Gltf/GlbReader.cs +++ b/src/MeshIO/Formats/Gltf/GlbReader.cs @@ -14,8 +14,6 @@ namespace MeshIO.Formats.Gltf; /// the reading process. public class GlbReader : SceneReader { - private GlbHeader _header; - /// public GlbReader(string path, GltfReaderOptions options = null, NotificationEventHandler notification = null) : base(path, options, notification) { } @@ -68,7 +66,7 @@ public override void Dispose() /// public override Scene Read() { - GlbHeader header = GlbHeader.Read(_stream.Stream); + GltfHeader header = GltfHeader.Read(_stream); IGlbFileBuilder reader; switch (header.Version) @@ -78,7 +76,7 @@ public override Scene Read() reader = new GlbFileBuilder(header); break; default: - throw new NotSupportedException($"Version {this._header.Version} not supported."); + throw new NotSupportedException($"Version {header.Version} not supported."); } reader.OnNotification += this.onNotificationEvent; diff --git a/src/MeshIO/Formats/Gltf/GltfReader.cs b/src/MeshIO/Formats/Gltf/GltfReader.cs new file mode 100644 index 0000000..e6bc6bc --- /dev/null +++ b/src/MeshIO/Formats/Gltf/GltfReader.cs @@ -0,0 +1,35 @@ +using CSUtilities.IO; +using MeshIO.Formats.Gltf.Readers; +using MeshIO.Formats.Gltf.Schema; +using MeshIO.Formats.Gltf.Schema.V2; +using System.IO; + +namespace MeshIO.Formats.Gltf; + +public class GltfReader : SceneReader +{ + private readonly StreamIO _bin; + + public GltfReader(string path, GltfReaderOptions options = null, NotificationEventHandler notification = null) + : base(path, options, notification) + { + var binFile = Path.ChangeExtension(path, ".bin"); + this._bin = new StreamIO(File.Open(binFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + } + + public GltfReader(Stream gltf, Stream gltfBinary, GltfReaderOptions options = null, NotificationEventHandler notification = null) + : base(gltf, options, notification) + { + this._bin = new StreamIO(gltfBinary); + } + + /// + public override Scene Read() + { + GltfHeader header = GltfHeader.Read(this._stream, this._bin); + GltfRoot root = header.GetRoot(); + var reader = new GlbFileBuilder(header); + reader.OnNotification += this.onNotificationEvent; + return reader.Build(); + } +} diff --git a/src/MeshIO/Formats/Gltf/Readers/GlbFileBuilder.cs b/src/MeshIO/Formats/Gltf/Readers/GlbFileBuilder.cs index c9054ee..3a860df 100644 --- a/src/MeshIO/Formats/Gltf/Readers/GlbFileBuilder.cs +++ b/src/MeshIO/Formats/Gltf/Readers/GlbFileBuilder.cs @@ -21,7 +21,7 @@ internal class GlbFileBuilder : IGlbFileBuilder private readonly Dictionary _cameras = new(); - private readonly GlbHeader _header; + private readonly GltfHeader _header; private readonly Dictionary _materials = new(); @@ -33,7 +33,7 @@ internal class GlbFileBuilder : IGlbFileBuilder private GltfRoot _root; - public GlbFileBuilder(GlbHeader header) + public GlbFileBuilder(GltfHeader header) { this._header = header; } @@ -181,7 +181,7 @@ private void createBuilders(Dictionary collectio for (int i = 0; i < gltfArray.Length; i++) { - var gltf = gltfArray[i]; + Gltf gltf = gltfArray[i]; Builder builder = new(); builder.GltfObject = gltf; diff --git a/src/MeshIO/Formats/Gltf/Schema/GlbHeader.cs b/src/MeshIO/Formats/Gltf/Schema/GltfHeader.cs similarity index 66% rename from src/MeshIO/Formats/Gltf/Schema/GlbHeader.cs rename to src/MeshIO/Formats/Gltf/Schema/GltfHeader.cs index 2cc78fc..1752df2 100644 --- a/src/MeshIO/Formats/Gltf/Schema/GlbHeader.cs +++ b/src/MeshIO/Formats/Gltf/Schema/GltfHeader.cs @@ -1,4 +1,5 @@ -using CSUtilities.Converters; +using CSUtilities; +using CSUtilities.Converters; using CSUtilities.IO; using MeshIO.Formats.Gltf.Schema.V2; using System.Collections.Generic; @@ -7,7 +8,7 @@ namespace MeshIO.Formats.Gltf.Schema; -internal class GlbHeader +internal class GltfHeader { public byte[] BinData { get; set; } @@ -17,14 +18,26 @@ internal class GlbHeader public uint Magic { get; set; } - public GltfVersion Version { get; set; } + public GltfVersion Version { get; set; } = GltfVersion.Unknown; - public static GlbHeader Read(Stream stream) + private GltfHeader() { - var reader = new StreamIO(stream); + } + + public static GltfHeader Read(StreamIO json, StreamIO bin) + { + var header = new GltfHeader(); + + header.JsonData = json.ReadBytes((int)json.Stream.Length); + header.BinData = bin.ReadBytes((int)bin.Stream.Length); + + return header; + } + public static GltfHeader Read(StreamIO reader) + { //The 12-byte header consists of three 4-byte entries: - var header = new GlbHeader(); + var header = new GltfHeader(); //magic equals 0x46546C67. It is ASCII string glTF, and can be used to identify data as Binary glTF. header.Magic = reader.ReadUInt(); //version indicates the version of the Binary glTF container format. This specification defines version 2. @@ -50,21 +63,30 @@ public static GlbHeader Read(Stream stream) public GltfRoot GetRoot() { - if (this.Version == GltfVersion.V1) - { - string json = Encoding.UTF8.GetString(JsonData); - var map = JsonUtils.Deserialize>(json); + string json = Encoding.UTF8.GetString(JsonData); - return new GltfRoot(map); - } - else + switch (this.Version) { - string json = Encoding.UTF8.GetString(JsonData); - return JsonUtils.Deserialize(json); + case GltfVersion.Unknown: + try + { + return JsonUtils.Deserialize(json); + } + catch (System.Exception) + { + return new GltfRoot(JsonUtils.Deserialize>(json)); + } + case GltfVersion.V1: + return new GltfRoot(JsonUtils.Deserialize>(json)); + case GltfVersion.V2: + return JsonUtils.Deserialize(json); + default: + throw new System.NotSupportedException(); } + } - private static void readV1Header(GlbHeader header, StreamIO reader) + private static void readV1Header(GltfHeader header, StreamIO reader) { uint totalLength = header.Length; int jsonLength = reader.ReadInt(); @@ -86,7 +108,7 @@ private static void readV1Header(GlbHeader header, StreamIO reader) reader.Stream.Read(header.BinData, 0, binLength); } - private static void readV2Heder(GlbHeader header, StreamIO reader) + private static void readV2Heder(GltfHeader header, StreamIO reader) { while (reader.Position < header.Length) { diff --git a/src/MeshIO/Formats/Gltf/Schema/GltfVersion.cs b/src/MeshIO/Formats/Gltf/Schema/GltfVersion.cs index 127af0f..6f211fb 100644 --- a/src/MeshIO/Formats/Gltf/Schema/GltfVersion.cs +++ b/src/MeshIO/Formats/Gltf/Schema/GltfVersion.cs @@ -2,6 +2,7 @@ internal enum GltfVersion { + Unknown = 0, V1 = 1, V2 = 2, } diff --git a/src/MeshIO/MeshIO.csproj b/src/MeshIO/MeshIO.csproj index 7cb5e68..bbc6ed8 100644 --- a/src/MeshIO/MeshIO.csproj +++ b/src/MeshIO/MeshIO.csproj @@ -27,7 +27,6 @@ - From 5e90c7a0b24130c85ca6a82a9b67239ef1f66c69 Mon Sep 17 00:00:00 2001 From: DomCR Date: Thu, 5 Feb 2026 11:25:09 +0100 Subject: [PATCH 3/4] docs --- src/MeshIO/Formats/Gltf/GlbReader.cs | 6 -- src/MeshIO/Formats/Gltf/GltfReader.cs | 79 ++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/MeshIO/Formats/Gltf/GlbReader.cs b/src/MeshIO/Formats/Gltf/GlbReader.cs index 4bd6b6b..64f42ef 100644 --- a/src/MeshIO/Formats/Gltf/GlbReader.cs +++ b/src/MeshIO/Formats/Gltf/GlbReader.cs @@ -57,12 +57,6 @@ public static Scene Read(Stream stream, GltfReaderOptions options = null, Notifi } } - /// - public override void Dispose() - { - base.Dispose(); - } - /// public override Scene Read() { diff --git a/src/MeshIO/Formats/Gltf/GltfReader.cs b/src/MeshIO/Formats/Gltf/GltfReader.cs index e6bc6bc..66b6f93 100644 --- a/src/MeshIO/Formats/Gltf/GltfReader.cs +++ b/src/MeshIO/Formats/Gltf/GltfReader.cs @@ -6,10 +6,26 @@ namespace MeshIO.Formats.Gltf; +/// +/// Provides functionality to read and parse glTF (GL Transmission Format) files and streams into scene objects. +/// +/// +/// The GltfReader supports reading standard glTF files. It can be +/// constructed from a file path or from streams, and allows for custom reader options and notification handling. +/// public class GltfReader : SceneReader { private readonly StreamIO _bin; + /// + /// Initializes a new instance of the GltfReader class to read a GLTF file and its associated binary data. + /// + /// The constructor automatically locates and opens the binary file associated with the specified GLTF + /// file. The reader supports concurrent access to the binary file if it is being used by other processes. + /// The path to the GLTF file to be read. Must not be null or empty. + /// Optional settings that control the behavior of the reader. If null, default options are used. + /// An optional event handler for receiving notifications during the reading process. If null, no notifications are + /// sent. public GltfReader(string path, GltfReaderOptions options = null, NotificationEventHandler notification = null) : base(path, options, notification) { @@ -17,12 +33,73 @@ public GltfReader(string path, GltfReaderOptions options = null, NotificationEve this._bin = new StreamIO(File.Open(binFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); } + /// + /// Initializes a new instance of the GltfReader class for reading glTF assets from the specified JSON and binary + /// streams. + /// + /// The caller is responsible for managing the lifetime of the provided streams. Both streams must + /// remain open for the duration of the GltfReader instance. + /// A stream containing the glTF JSON content. The stream must be readable and positioned at the start of the glTF + /// data. + /// A stream containing the binary buffer data associated with the glTF asset. The stream must be readable and + /// positioned at the start of the binary data. + /// Optional settings that control how the glTF asset is read and processed. If null, default options are used. + /// An optional event handler for receiving notifications or warnings during the reading process. If null, no + /// notifications are raised. public GltfReader(Stream gltf, Stream gltfBinary, GltfReaderOptions options = null, NotificationEventHandler notification = null) : base(gltf, options, notification) { this._bin = new StreamIO(gltfBinary); } + /// + /// Reads a glTF file from the specified path and returns the parsed scene. + /// + /// This method provides a convenient way to load a glTF file from disk in a single call. The caller is + /// responsible for ensuring that the file exists and is accessible. If the file is invalid or cannot be parsed, an + /// exception may be thrown. + /// The file system path to the glTF file to read. Must not be null or empty. + /// Optional settings that control how the glTF file is read and parsed. If null, default options are used. + /// An optional event handler for receiving notifications or warnings during the reading process. If null, + /// notifications are ignored. + /// A Scene object representing the contents of the glTF file. + public static Scene Read(string path, GltfReaderOptions options = null, NotificationEventHandler notification = null) + { + using (GltfReader reader = new GltfReader(path, options, notification)) + { + return reader.Read(); + } + } + + /// + /// Reads a glTF scene from the specified stream and optional binary buffer, using the provided options and + /// notification handler. + /// + /// The caller is responsible for managing the lifetime of the input streams. This method does not + /// close or dispose the provided streams. + /// The stream containing the glTF JSON content to read. The stream must be readable and positioned at the start of the + /// glTF data. + /// An optional stream containing the binary buffer referenced by the glTF file, or null if the glTF does not use an + /// external binary buffer. The stream must be readable and positioned at the start of the binary data. + /// Optional settings that control how the glTF is read and processed. If null, default options are used. + /// An optional event handler for receiving notifications or warnings encountered during reading. If null, + /// notifications are ignored. + /// A Scene object representing the parsed glTF scene. Returns null if the scene could not be read or is invalid. + public static Scene Read(Stream stream, Stream gltfBinary, GltfReaderOptions options = null, NotificationEventHandler notification = null) + { + using (GltfReader reader = new GltfReader(stream, gltfBinary, options, notification)) + { + return reader.Read(); + } + } + + /// + public override void Dispose() + { + base.Dispose(); + this._bin.Dispose(); + } + /// public override Scene Read() { @@ -32,4 +109,4 @@ public override Scene Read() reader.OnNotification += this.onNotificationEvent; return reader.Build(); } -} +} \ No newline at end of file From 86c03488ce9ae83a25c8ebfc4167fa1492d52624 Mon Sep 17 00:00:00 2001 From: DomCR Date: Thu, 5 Feb 2026 11:28:55 +0100 Subject: [PATCH 4/4] test fix --- src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs b/src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs index f28a2fe..fa4cf22 100644 --- a/src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs +++ b/src/MeshIO.Tests/Formats/Gltf/GltfReaderLocalTest.cs @@ -26,6 +26,8 @@ static GltfReaderLocalTest() { GlbV1Files.Add(new FileModel()); GlbV2Files.Add(new FileModel()); + GltfV1Files.Add(new FileModel()); + GltfV2Files.Add(new FileModel()); return; }