Initial commit.
This commit is contained in:
commit
8256444c76
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# common editors
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.rider
|
||||
**/.atom
|
||||
**/.idea
|
||||
|
||||
# build artifacts
|
||||
**/obj
|
||||
**/bin
|
||||
|
||||
doc/html/**
|
||||
doc/latex/**
|
||||
|
22
ReFuel.Gltf.sln
Normal file
22
ReFuel.Gltf.sln
Normal 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
59
ReFuel.Gltf/GlbStructures.cs
Executable 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
185
ReFuel.Gltf/GltfAccessor.cs
Executable 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
13
ReFuel.Gltf/GltfAccessorType.cs
Executable 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
68
ReFuel.Gltf/GltfAsset.cs
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
14
ReFuel.Gltf/GltfAttributeType.cs
Executable file
14
ReFuel.Gltf/GltfAttributeType.cs
Executable 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
88
ReFuel.Gltf/GltfBuffer.cs
Executable 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
96
ReFuel.Gltf/GltfBufferView.cs
Executable 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
134
ReFuel.Gltf/GltfCollection.cs
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
ReFuel.Gltf/GltfComponentType.cs
Executable file
12
ReFuel.Gltf/GltfComponentType.cs
Executable 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
407
ReFuel.Gltf/GltfDocument.cs
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
27
ReFuel.Gltf/GltfEmptyExtensionProvider.cs
Executable file
27
ReFuel.Gltf/GltfEmptyExtensionProvider.cs
Executable 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
38
ReFuel.Gltf/GltfExtensions.cs
Executable 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
209
ReFuel.Gltf/GltfIdCollection.cs
Executable 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
105
ReFuel.Gltf/GltfImage.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
22
ReFuel.Gltf/GltfJsonSerializer.cs
Normal file
22
ReFuel.Gltf/GltfJsonSerializer.cs
Normal 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
313
ReFuel.Gltf/GltfMaterial.cs
Normal 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
250
ReFuel.Gltf/GltfMesh.cs
Normal 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
13
ReFuel.Gltf/GltfMeshMode.cs
Executable 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
|
||||
}
|
||||
}
|
11
ReFuel.Gltf/GltfMiscObject.cs
Normal file
11
ReFuel.Gltf/GltfMiscObject.cs
Normal 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
407
ReFuel.Gltf/GltfNode.cs
Executable 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
86
ReFuel.Gltf/GltfObject.cs
Executable 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.");
|
||||
}
|
||||
}
|
||||
}
|
13
ReFuel.Gltf/GltfPrimitiveTopology.cs
Executable file
13
ReFuel.Gltf/GltfPrimitiveTopology.cs
Executable 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
|
||||
}
|
||||
}
|
93
ReFuel.Gltf/GltfSampler.cs
Normal file
93
ReFuel.Gltf/GltfSampler.cs
Normal 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
49
ReFuel.Gltf/GltfScene.cs
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
45
ReFuel.Gltf/GltfSerializedExtensionProvider.cs
Executable file
45
ReFuel.Gltf/GltfSerializedExtensionProvider.cs
Executable 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
81
ReFuel.Gltf/GltfSkin.cs
Normal 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
9
ReFuel.Gltf/GltfTargetHint.cs
Executable file
@ -0,0 +1,9 @@
|
||||
namespace ReFuel.Gltf
|
||||
{
|
||||
public enum GltfTargetHint
|
||||
{
|
||||
None,
|
||||
ArrayBuffer = 34962,
|
||||
ElementArrayBuffer = 3463
|
||||
}
|
||||
}
|
74
ReFuel.Gltf/GltfTexture.cs
Normal file
74
ReFuel.Gltf/GltfTexture.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
20
ReFuel.Gltf/IGltfExtensionProvider.cs
Executable file
20
ReFuel.Gltf/IGltfExtensionProvider.cs
Executable 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);
|
||||
}
|
||||
}
|
18
ReFuel.Gltf/IGltfStreamProvider.cs
Normal file
18
ReFuel.Gltf/IGltfStreamProvider.cs
Normal 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
62
ReFuel.Gltf/IO/GltfUriReader.cs
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
98
ReFuel.Gltf/IO/StreamWrapper.cs
Normal file
98
ReFuel.Gltf/IO/StreamWrapper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
10
ReFuel.Gltf/ReFuel.Gltf.csproj
Normal file
10
ReFuel.Gltf/ReFuel.Gltf.csproj
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user