Initial commit.

This commit is contained in:
H. Utku Maden 2025-10-12 14:33:16 +03:00
commit 8256444c76
36 changed files with 3165 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
# common editors
**/.vs
**/.vscode
**/.rider
**/.atom
**/.idea
# build artifacts
**/obj
**/bin
doc/html/**
doc/latex/**

0
README.md Normal file
View File

22
ReFuel.Gltf.sln Normal file
View File

@ -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

59
ReFuel.Gltf/GlbStructures.cs Executable file
View File

@ -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<byte>(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<byte>(pchunk, sizeof(GlbChunk))) == sizeof(GlbChunk);
}
}
public enum GlbChunkType : int
{
Json = 0x4E4F534A,
Bin = 0x004E4942,
}
}

185
ReFuel.Gltf/GltfAccessor.cs Executable file
View File

@ -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<GltfBufferView> 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<GltfBufferView>(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<float> floats = new List<float>();
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<float> floats = new List<float>();
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();
}
}
}

13
ReFuel.Gltf/GltfAccessorType.cs Executable file
View File

@ -0,0 +1,13 @@
namespace ReFuel.Gltf
{
public enum GltfAccessorType
{
Scalar,
Vec2,
Vec3,
Vec4,
Mat2,
Mat3,
Mat4
}
}

68
ReFuel.Gltf/GltfAsset.cs Executable file
View File

@ -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();
}
}
}

View File

@ -0,0 +1,14 @@
namespace ReFuel.Gltf
{
public enum GltfAttributeType
{
Position,
Normal,
Tangent,
TexCoord,
Color,
Joints,
Weights,
Custom
}
}

88
ReFuel.Gltf/GltfBuffer.cs Executable file
View File

@ -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();
}
}
}

96
ReFuel.Gltf/GltfBufferView.cs Executable file
View File

@ -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<GltfBuffer> 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<GltfBuffer>(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();
}
}
}

134
ReFuel.Gltf/GltfCollection.cs Executable file
View File

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace ReFuel.Gltf
{
/// <summary>
/// An empty object for storing glTF extra and extension properties.
/// </summary>
public class GltfCollection : Dictionary<string, object?>
{
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<object?>
{
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;
}
}
}
}
}

View File

@ -0,0 +1,12 @@
namespace ReFuel.Gltf
{
public enum GltfComponentType
{
Byte = 5120,
UnsignedByte = 5121,
Short = 5122,
UnsignedShort = 5123,
UnsignedInt = 5125,
Float = 5126
}
}

407
ReFuel.Gltf/GltfDocument.cs Executable file
View File

@ -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,
}
/// <summary>
/// glTF Document Object GLObjects
/// </summary>
[JsonConverter(typeof(GltfJsonConverter))]
public class GltfDocument : GltfObject, IDisposable
{
public override GltfObjectKind Kind => GltfObjectKind.Document;
public List<Stream> BinaryStreams { get; private set; }
public int DefaultScene { get; set; } = 0;
public HashSet<string> Extensions { get; set; } = new HashSet<string>();
public HashSet<string> Required { get; set; } = new HashSet<string>();
public GltfAsset Asset { get; }
public GltfIdList<GltfScene> Scenes { get; } = new GltfIdList<GltfScene>();
public GltfIdList<GltfNode> Nodes { get; } = new GltfIdList<GltfNode>();
public GltfIdList<GltfMesh> Meshes { get; } = new GltfIdList<GltfMesh>();
public GltfIdList<GltfBuffer> Buffers { get; } = new GltfIdList<GltfBuffer>();
public GltfIdList<GltfBufferView> BufferViews { get; } = new GltfIdList<GltfBufferView>();
public GltfIdList<GltfAccessor> Accessors { get; } = new GltfIdList<GltfAccessor>();
public GltfIdList<GltfMaterial> Materials { get; } = new GltfIdList<GltfMaterial>();
public GltfIdList<GltfTexture> Textures { get; } = new GltfIdList<GltfTexture>();
public GltfIdList<GltfSampler> Samplers { get; } = new GltfIdList<GltfSampler>();
public GltfIdList<GltfImage> Images { get; } = new GltfIdList<GltfImage>();
public GltfIdList<GltfSkin> Skins { get; } = new GltfIdList<GltfSkin>();
private GltfDocument(JsonDocument doc, IEnumerable<StreamWrapper>? binaryStreams = null)
{
Document = this;
Asset = new GltfAsset(this);
if (binaryStreams == null)
BinaryStreams = new List<Stream>();
else
BinaryStreams = new List<Stream>(binaryStreams);
JsonElement root = doc.RootElement;
Deserialize(root);
}
private static void WriteStringSet(HashSet<string> 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<StreamWrapper> binaryStreams = new List<StreamWrapper>();
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<string, IGltfExtensionProvider> extensionProviders = new Dictionary<string, IGltfExtensionProvider>();
private static HashSet<string> supportedExtensions = new HashSet<string>();
public static IReadOnlySet<string> 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<T>() where T : IGltfExtensionProvider, new()
{
AddExtension(new T());
}
public static void AddExtension(IEnumerable<IGltfExtensionProvider> 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<string, JsonElement> elements = new Dictionary<string, JsonElement>();
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();
}
}
}

View File

@ -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();
}
}
}

38
ReFuel.Gltf/GltfExtensions.cs Executable file
View File

@ -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<string> BaseExtensions { get; }
static GltfExtensions()
{
Type t = typeof(GltfExtensions);
IEnumerable<string> fields = t.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(x => x.FieldType == typeof(string))
.Select(x => x.Name);
BaseExtensions = ImmutableHashSet.Create(fields.ToArray());
}
}
}

209
ReFuel.Gltf/GltfIdCollection.cs Executable file
View File

@ -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<T> : IReadOnlyList<T>
where T : GltfIdObject
{
private readonly List<T> items = new List<T>();
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<T> GetEnumerator()
{
return items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return items.GetEnumerator();
}
}
public class GltfRefList<T> : ICollection<T>, IReadOnlyList<T> where T : GltfIdObject
{
private readonly List<ItemTuple> items = new List<ItemTuple>();
private readonly GltfIdList<T> list;
public IEnumerable<int> 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<T> 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<T> 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<T> where T : GltfIdObject
{
private readonly GltfIdList<T> 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<T> list)
{
this.list = list;
}
public void Reset()
{
id = -1;
value = null;
}
public static implicit operator T(GltfSmartId<T> id) => id.Value;
}
}

105
ReFuel.Gltf/GltfImage.cs Normal file
View File

@ -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<GltfBufferView> _bufferView = new GltfSmartId<GltfBufferView>();
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();
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ReFuel.Gltf
{
public class GltfJsonConverter : JsonConverter<GltfDocument>
{
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);
}
}
}

313
ReFuel.Gltf/GltfMaterial.cs Normal file
View File

@ -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<GltfTexture> _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<GltfTexture>(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
};
}
}

250
ReFuel.Gltf/GltfMesh.cs Normal file
View File

@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace ReFuel.Gltf
{
public record struct GltfMeshAttribute
{
private GltfSmartId<GltfAccessor> 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<GltfAccessor>(document.Accessors)
{
Id = accessorIndex,
};
ReadOnlySpan<char> full = name.AsSpan();
ReadOnlySpan<char> head;
ReadOnlySpan<char> tail;
int indexOfUnderscore = name.IndexOf('_');
if (indexOfUnderscore == -1)
{
head = full;
tail = ReadOnlySpan<char>.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<GltfAccessor> index;
private GltfSmartId<GltfMaterial> material;
public List<GltfMeshAttribute> Attributes { get; } = new List<GltfMeshAttribute>();
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<GltfIdObject> Targets { get; private set; }
internal GltfMeshPrimitive(GltfDocument document) : base(document)
{
Targets = new GltfRefList<GltfIdObject>(null!);
index = new GltfSmartId<GltfAccessor>(document.Accessors);
material = new GltfSmartId<GltfMaterial>(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<GltfMeshPrimitive> Primitives { get; } = new List<GltfMeshPrimitive>();
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<float> values = new List<float>();
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();
}
}
}

13
ReFuel.Gltf/GltfMeshMode.cs Executable file
View File

@ -0,0 +1,13 @@
namespace ReFuel.Gltf
{
public enum GltfMeshMode
{
Points = 0,
Lines = 1,
LineLoop = 2,
LineStrip = 3,
Triangles = 4,
TriangleStrip = 5,
TriangleFan = 6
}
}

View File

@ -0,0 +1,11 @@
namespace ReFuel.Gltf
{
public abstract class GtlfMiscObject : GltfObject
{
public override GltfObjectKind Kind => GltfObjectKind.Misc;
protected GtlfMiscObject(GltfDocument document) : base(document)
{
}
}
}

407
ReFuel.Gltf/GltfNode.cs Executable file
View File

@ -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<GltfIdObject> camera;
private GltfSmartId<GltfIdObject> skin;
private GltfSmartId<GltfMesh> 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<GltfNode> 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<GltfNode>(document.Nodes);
mesh = new GltfSmartId<GltfMesh>(document.Meshes);
}
/// <summary>
/// Optimize this node for matrix mode transforms.
/// </summary>
/// <returns>Self</returns>
public GltfNode MatrixMode()
{
if (transformMode == TransformMode.Matrix)
return this;
transformMode = TransformMode.Matrix;
transform = transform.ToMatrix();
return this;
}
/// <summary>
/// Optimize this node for scale rotation translation transforms.
/// </summary>
/// <returns>Self</returns>
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
}
}
}

86
ReFuel.Gltf/GltfObject.cs Executable file
View File

@ -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.");
}
}
}

View File

@ -0,0 +1,13 @@
namespace ReFuel.Gltf
{
public enum GltfPrimitiveTopology
{
Points = 0,
Lines = 1,
LineLoop = 2,
LineStrip = 3,
Triangles = 4,
TriangleStrip = 5,
TriangleFan = 6
}
}

View File

@ -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();
}
}
}

49
ReFuel.Gltf/GltfScene.cs Executable file
View File

@ -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<GltfNode> Nodes { get; }
internal GltfScene(GltfDocument document) : base(document)
{
Nodes = new GltfRefList<GltfNode>(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();
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ReFuel.Gltf
{
public class GltfSerializedExtensionProvider<T> : IGltfExtensionProvider<T>
{
public string Name { get; }
public Type? DataType { get; } = typeof(T);
public GltfSerializedExtensionProvider(string name)
{
Name = name;
if (typeof(T).GetCustomAttribute<JsonSerializableAttribute>() is null)
throw new Exception("This type is not JSON serializable.");
}
public T ParseExtension(JsonElement element)
{
return JsonSerializer.Deserialize<T>(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()}.");
}
}
}

81
ReFuel.Gltf/GltfSkin.cs Normal file
View File

@ -0,0 +1,81 @@
using System;
using System.Text.Json;
namespace ReFuel.Gltf
{
public class GltfSkin : GltfIdObject
{
private GltfSmartId<GltfAccessor> _inverseBindMatrices = new GltfSmartId<GltfAccessor>();
private GltfSmartId<GltfNode> _skeleton = new GltfSmartId<GltfNode>();
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<GltfNode> Joints { get; }
internal GltfSkin(GltfDocument document) : base(document)
{
Joints = new GltfRefList<GltfNode>(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();
}
}
}

9
ReFuel.Gltf/GltfTargetHint.cs Executable file
View File

@ -0,0 +1,9 @@
namespace ReFuel.Gltf
{
public enum GltfTargetHint
{
None,
ArrayBuffer = 34962,
ElementArrayBuffer = 3463
}
}

View File

@ -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<GltfSampler> _sampler;
private GltfSmartId<GltfImage> _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<GltfSampler>(document.Samplers);
_source = new GltfSmartId<GltfImage>(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();
}
}
}

View File

@ -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<T> : IGltfExtensionProvider
{
T ParseExtension(JsonElement element);
void WriteExtension(T value, Utf8JsonWriter writer);
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.IO;
namespace ReFuel.Gltf
{
/// <summary>
/// Provides users with special stream opening logic.
/// </summary>
public interface IGltfStreamProvider
{
/// <summary>
/// Open the file at the given URI.
/// </summary>
/// <param name="pwd">The present working directory of the file.</param>
/// <param name="uri">The URI of the file to open</param>
public Stream? OpenFile(string? pwd, Uri uri);
}
}

62
ReFuel.Gltf/IO/GltfUriReader.cs Executable file
View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.1</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>