commit
49fee37cf5
10 changed files with 738 additions and 0 deletions
@ -0,0 +1,4 @@ |
|||||||
|
/.vscode/ |
||||||
|
/bin/ |
||||||
|
/obj/ |
||||||
|
/test/ |
@ -0,0 +1,278 @@ |
|||||||
|
using System.Runtime.InteropServices; |
||||||
|
using System.Text; |
||||||
|
using System.Text.Json.Nodes; |
||||||
|
|
||||||
|
namespace Res2tf; |
||||||
|
|
||||||
|
using List = List<BsonValue>; |
||||||
|
using Dict = Dictionary<string, BsonValue>; |
||||||
|
|
||||||
|
public enum ElementType : byte |
||||||
|
{ |
||||||
|
Double = 1, |
||||||
|
String, |
||||||
|
Object, |
||||||
|
Array, |
||||||
|
Binary, |
||||||
|
Undefined, // deprecated |
||||||
|
ObjectId, // unsupported |
||||||
|
Boolean, |
||||||
|
DateTime, |
||||||
|
Null, |
||||||
|
RegExp, // unsupported |
||||||
|
DbPointer, // deprecated |
||||||
|
JsCode, // unsupported |
||||||
|
Symbol, // deprecated |
||||||
|
JsCodeScope, // deprecated |
||||||
|
Int32, |
||||||
|
Timestamp, // unsupported |
||||||
|
Int64, |
||||||
|
Decimal128, // unsupported |
||||||
|
}; |
||||||
|
|
||||||
|
public class BsonValue : IEquatable<BsonValue> |
||||||
|
{ |
||||||
|
public static readonly BsonValue Null = new(ElementType.Null, null); |
||||||
|
|
||||||
|
public ElementType ElementType { get; } |
||||||
|
public object? Value { get; } |
||||||
|
|
||||||
|
public int Count => AsCollection().Count; |
||||||
|
public BsonValue this[int index] { get => AsList()[index]; set => AsList()[index] = value; } |
||||||
|
public BsonValue this[string key] { get => AsDict()[key ]; set => AsDict()[key ] = value; } |
||||||
|
public Dict.KeyCollection Keys => AsDict().Keys; |
||||||
|
public Dict.ValueCollection Values => AsDict().Values; |
||||||
|
|
||||||
|
private BsonValue(ElementType type, object? value) { ElementType = type; Value = value; } |
||||||
|
|
||||||
|
public BsonValue(double value) : this(ElementType.Double , value) { } |
||||||
|
public BsonValue(string value) : this(ElementType.String , value) { ArgumentNullException.ThrowIfNull(value); } |
||||||
|
public BsonValue(byte[] value) : this(ElementType.Binary , value) { ArgumentNullException.ThrowIfNull(value); } |
||||||
|
public BsonValue(bool value) : this(ElementType.Boolean , value) { } |
||||||
|
public BsonValue(DateTime value) : this(ElementType.DateTime, value) { } |
||||||
|
public BsonValue(int value) : this(ElementType.Int32 , value) { } |
||||||
|
public BsonValue(long value) : this(ElementType.Int64 , value) { } |
||||||
|
|
||||||
|
public static BsonValue Array() => new(ElementType.Array, new List()); |
||||||
|
public static BsonValue Array(params IEnumerable<BsonValue> values) => new(ElementType.Array, values.ToList()); |
||||||
|
|
||||||
|
public static BsonValue Object() => new(ElementType.Object, new Dict()); |
||||||
|
public static BsonValue Object(IDictionary<string, BsonValue> entries) => new(ElementType.Object, entries.ToDictionary()); |
||||||
|
public static BsonValue Object(params IEnumerable<(string, BsonValue)> entries) => new(ElementType.Object, entries.ToDictionary()); |
||||||
|
|
||||||
|
public List AsList() => (Value as List) ?? throw new InvalidOperationException($"Attempting to access {ElementType} as Array"); |
||||||
|
public Dict AsDict() => (Value as Dict) ?? throw new InvalidOperationException($"Attempting to access {ElementType} as Object"); |
||||||
|
protected ICollection<BsonValue> AsCollection() => Value switch { List list => list, Dict dict => dict.Values, _ => throw new NotSupportedException($"Attempting to access {ElementType} as Array or Object"), }; |
||||||
|
|
||||||
|
public JsonNode? ToJson() |
||||||
|
=> Value switch { |
||||||
|
Dict v => new JsonObject(AsDict().Select(e => new KeyValuePair<string, JsonNode?>(e.Key, e.Value.ToJson()))), |
||||||
|
List v => new JsonArray (AsList().Select(e => e.ToJson()).ToArray()), |
||||||
|
double v => JsonValue.Create(v), |
||||||
|
string v => JsonValue.Create(v)!, |
||||||
|
byte[] v => JsonValue.Create(v)!, |
||||||
|
bool v => JsonValue.Create(v), |
||||||
|
DateTime v => JsonValue.Create(v), |
||||||
|
int v => JsonValue.Create(v), |
||||||
|
long v => JsonValue.Create(v), |
||||||
|
null => null, |
||||||
|
_ => throw new NotImplementedException(), |
||||||
|
}; |
||||||
|
|
||||||
|
public T[] ToArray<T>() |
||||||
|
{ |
||||||
|
if (typeof(T) == typeof(BsonValue)) return (T[])(object)AsList().ToArray(); |
||||||
|
if (typeof(T) == typeof(byte) && ElementType == ElementType.Binary) return (T[])Value!; |
||||||
|
if (typeof(T) == typeof(float)) return AsList().Select(v => (T)(object)(float)(double)v.Value!).ToArray(); |
||||||
|
if (ElementTypeOf<T>() == null) throw new NotSupportedException($"{typeof(T)} is not a valid ElementType"); |
||||||
|
return AsList().Select(v => (T)v.Value!).ToArray(); |
||||||
|
} |
||||||
|
|
||||||
|
public Dictionary<string, T> ToDictionary<T>() |
||||||
|
{ |
||||||
|
if (typeof(T) == typeof(BsonValue)) return (Dictionary<string, T>)(object)AsDict().ToDictionary(); |
||||||
|
if (ElementTypeOf<T>() == null) throw new NotSupportedException($"{typeof(T)} is not a valid ElementType"); |
||||||
|
return AsDict().ToDictionary(e => e.Key, e => (T)e.Value.Value!); |
||||||
|
} |
||||||
|
|
||||||
|
public BsonValue? GetOrNull(string key) => AsDict().GetValueOrDefault(key); |
||||||
|
|
||||||
|
public void Add(BsonValue value) => AsList().Add(value); |
||||||
|
public void Add(string key, BsonValue value) => AsDict().Add(key, value); |
||||||
|
|
||||||
|
public bool Contains(BsonValue value) => AsList().Contains(value); |
||||||
|
public bool ContainsKey(string key) => AsDict().ContainsKey(key); |
||||||
|
|
||||||
|
public void RemoveAt(int index) => AsList().RemoveAt(index); |
||||||
|
public bool Remove(string key) => AsDict().Remove(key); |
||||||
|
|
||||||
|
public void Clear() |
||||||
|
{ |
||||||
|
switch (Value) { |
||||||
|
case List list: list.Clear(); break; |
||||||
|
case Dict dict: dict.Clear(); break; |
||||||
|
default: throw new NotSupportedException($"Attempting to access {ElementType} as Array or Object"); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
public override bool Equals(object? obj) |
||||||
|
=> Equals(obj as BsonValue); |
||||||
|
public bool Equals(BsonValue? other) |
||||||
|
=> (other is not null) |
||||||
|
&& (ElementType == other.ElementType) |
||||||
|
&& ElementType switch { |
||||||
|
ElementType.Array => Enumerable.SequenceEqual(AsList(), other.AsList()), |
||||||
|
ElementType.Object => DictionaryEquals(AsDict(), other.AsDict()), |
||||||
|
_ => Equals(Value, other.Value), |
||||||
|
}; |
||||||
|
public override int GetHashCode() |
||||||
|
=> HashCode.Combine(ElementType, Value); |
||||||
|
|
||||||
|
public static bool operator ==(BsonValue? left, BsonValue? right) |
||||||
|
=> ReferenceEquals(left, right) || (left?.Equals(right) ?? false); |
||||||
|
public static bool operator !=(BsonValue? left, BsonValue? right) |
||||||
|
=> !(left == right); |
||||||
|
|
||||||
|
public static implicit operator BsonValue(double value) => new(value); |
||||||
|
public static implicit operator BsonValue(string value) => new(value); |
||||||
|
public static implicit operator BsonValue(byte[] value) => new(value); |
||||||
|
public static implicit operator BsonValue(bool value) => new(value); |
||||||
|
public static implicit operator BsonValue(DateTime value) => new(value); |
||||||
|
public static implicit operator BsonValue(int value) => new(value); |
||||||
|
public static implicit operator BsonValue(long value) => new(value); |
||||||
|
|
||||||
|
public static explicit operator double (BsonValue value) => (double )value.Value!; |
||||||
|
public static explicit operator string (BsonValue value) => (string )value.Value!; |
||||||
|
public static explicit operator byte[] (BsonValue value) => (byte[] )value.Value!; |
||||||
|
public static explicit operator bool (BsonValue value) => (bool )value.Value!; |
||||||
|
public static explicit operator DateTime(BsonValue value) => (DateTime)value.Value!; |
||||||
|
public static explicit operator int (BsonValue value) => (int )value.Value!; |
||||||
|
public static explicit operator long (BsonValue value) => (long )value.Value!; |
||||||
|
|
||||||
|
public static explicit operator float(BsonValue value) => (float)(double)value; |
||||||
|
|
||||||
|
private static ElementType? ElementTypeOf<T>() |
||||||
|
{ |
||||||
|
if (typeof(T) == typeof(double )) return ElementType.Double; |
||||||
|
if (typeof(T) == typeof(string )) return ElementType.String; |
||||||
|
if (typeof(T) == typeof(Dict )) return ElementType.Object; |
||||||
|
if (typeof(T) == typeof(List )) return ElementType.Array; |
||||||
|
if (typeof(T) == typeof(byte[] )) return ElementType.Binary; |
||||||
|
if (typeof(T) == typeof(bool )) return ElementType.Boolean; |
||||||
|
if (typeof(T) == typeof(DateTime)) return ElementType.DateTime; |
||||||
|
if (typeof(T) == typeof(int )) return ElementType.Int32; |
||||||
|
if (typeof(T) == typeof(long )) return ElementType.Int64; |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private static bool DictionaryEquals(Dict first, Dict second) |
||||||
|
=> (first.Count == second.Count) && !first.Except(second).Any(); |
||||||
|
} |
||||||
|
|
||||||
|
public class BsonReader : IDisposable |
||||||
|
{ |
||||||
|
private readonly BinaryReader _reader; |
||||||
|
private readonly List<byte> _cStringBuf = []; |
||||||
|
|
||||||
|
public BsonReader(Stream stream, bool leaveOpen = false) |
||||||
|
{ |
||||||
|
if (!stream.CanRead) throw new ArgumentException("stream is not readable"); |
||||||
|
_reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen); |
||||||
|
} |
||||||
|
|
||||||
|
public void Dispose() |
||||||
|
{ |
||||||
|
_reader.Dispose(); |
||||||
|
GC.SuppressFinalize(this); |
||||||
|
} |
||||||
|
|
||||||
|
public static BsonValue Load(Stream stream, bool leaveOpen = false) |
||||||
|
{ |
||||||
|
using var reader = new BsonReader(stream, leaveOpen); |
||||||
|
return reader.ReadObject(); |
||||||
|
} |
||||||
|
public static BsonValue Load(byte[] bytes) |
||||||
|
{ |
||||||
|
using var stream = new MemoryStream(bytes); |
||||||
|
return Load(stream); |
||||||
|
} |
||||||
|
|
||||||
|
public BsonValue ReadObject() |
||||||
|
{ |
||||||
|
// A "document" is prefixed with the number of bytes it takes up, but |
||||||
|
// we can just ignore it and read elements until we detect a NUL byte. |
||||||
|
// Thankfully we don't need to write a BsonWriter ... |
||||||
|
_reader.ReadInt32(); // length, unused |
||||||
|
|
||||||
|
var result = BsonValue.Object(); |
||||||
|
while (ReadElement() is (string name, BsonValue value)) |
||||||
|
result.Add(name, value); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public BsonValue ReadArray() |
||||||
|
{ |
||||||
|
// An "array" is just an object with stringified integer keys ... |
||||||
|
_reader.ReadInt32(); // length, unused |
||||||
|
|
||||||
|
var result = BsonValue.Array(); |
||||||
|
while (ReadElement() is (string name, BsonValue value)) |
||||||
|
{ |
||||||
|
if (!int.TryParse(name, out var index)) throw new Exception("Couldn't parse array index as integer"); |
||||||
|
if (index != result.Count) throw new Exception("Non-sequential array index"); |
||||||
|
result.Add(value); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public BsonValue ReadBinary() |
||||||
|
{ |
||||||
|
var length = _reader.ReadInt32(); |
||||||
|
_reader.ReadByte(); // subtype, unused |
||||||
|
return _reader.ReadBytes(length); |
||||||
|
} |
||||||
|
|
||||||
|
public (string Name, BsonValue Value)? ReadElement() |
||||||
|
{ |
||||||
|
var type = (ElementType)_reader.ReadByte(); |
||||||
|
if (type == 0) return null; |
||||||
|
|
||||||
|
var name = ReadCString(); |
||||||
|
return (name, type switch |
||||||
|
{ |
||||||
|
ElementType.Double => _reader.ReadDouble(), |
||||||
|
ElementType.String => ReadLengthPrefixedString(), |
||||||
|
ElementType.Object => ReadObject(), |
||||||
|
ElementType.Array => ReadArray(), |
||||||
|
ElementType.Binary => ReadBinary(), |
||||||
|
ElementType.Boolean => _reader.ReadByte() != 0, |
||||||
|
ElementType.DateTime => DateTime.UnixEpoch + TimeSpan.FromMilliseconds(_reader.ReadInt64()), |
||||||
|
ElementType.Null => BsonValue.Null, |
||||||
|
ElementType.Int32 => _reader.ReadInt32(), |
||||||
|
ElementType.Int64 => _reader.ReadInt64(), |
||||||
|
|
||||||
|
ElementType.Undefined | ElementType.ObjectId | ElementType.RegExp |
||||||
|
| ElementType.DbPointer | ElementType.JsCode | ElementType.Symbol |
||||||
|
| ElementType.JsCodeScope | ElementType.Timestamp | ElementType.Decimal128 |
||||||
|
=> throw new NotSupportedException($"Reading {type} is not supported"), |
||||||
|
|
||||||
|
_ => throw new Exception($"Unknown ElementType {type}"), |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private string ReadLengthPrefixedString() |
||||||
|
{ |
||||||
|
var length = _reader.ReadInt32(); |
||||||
|
var bytes = _reader.ReadBytes(length); |
||||||
|
if (bytes[^1] != 0) throw new Exception("Trailing byte in string must be NUL"); |
||||||
|
return Encoding.UTF8.GetString(bytes.AsSpan()[0..^1]); |
||||||
|
} |
||||||
|
|
||||||
|
private string ReadCString() |
||||||
|
{ |
||||||
|
_cStringBuf.Clear(); |
||||||
|
byte b; while ((b = _reader.ReadByte()) != 0) _cStringBuf.Add(b); |
||||||
|
var bytes = CollectionsMarshal.AsSpan(_cStringBuf); |
||||||
|
return Encoding.UTF8.GetString(bytes); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
using System.IO.Compression; |
||||||
|
using System.Numerics; |
||||||
|
using System.Text; |
||||||
|
|
||||||
|
namespace Res2tf; |
||||||
|
|
||||||
|
public class DataTree |
||||||
|
{ |
||||||
|
public BsonValue Bson { get; } |
||||||
|
public string[] Types { get; } |
||||||
|
public DataTreeNode Root { get; } |
||||||
|
|
||||||
|
// Technically unused, but kept in case it will be needed. |
||||||
|
public Dictionary<string, DataTreeNode> Nodes { get; } = []; |
||||||
|
|
||||||
|
public DataTree(string path) : this(LoadBson(path)) { } |
||||||
|
public DataTree(Stream stream) : this(LoadBson(stream)) { } |
||||||
|
public DataTree(BsonValue bson) |
||||||
|
{ |
||||||
|
Bson = bson; |
||||||
|
Types = bson["Types"].ToArray<string>(); |
||||||
|
Root = CreateNodeRecursive(null, bson["Object"]); |
||||||
|
} |
||||||
|
|
||||||
|
public int? GetTypeId(string type) |
||||||
|
=> Array.IndexOf(Types, type) switch |
||||||
|
{ -1 => null, int i => i }; |
||||||
|
|
||||||
|
DataTreeNode CreateNodeRecursive(DataTreeNode? parent, BsonValue bson) |
||||||
|
{ |
||||||
|
var node = new DataTreeNode(this, parent, bson); |
||||||
|
foreach (var childBson in bson.GetOrNull("Children")?.AsList() ?? []) |
||||||
|
node.Children.Add(CreateNodeRecursive(node, childBson)); |
||||||
|
Nodes[node.ID] = node; |
||||||
|
return node; |
||||||
|
} |
||||||
|
|
||||||
|
static BsonValue LoadBson(string path) |
||||||
|
=> LoadBson(File.OpenRead(path)); |
||||||
|
static BsonValue LoadBson(Stream stream) |
||||||
|
{ |
||||||
|
// Read and verify file header. |
||||||
|
using var reader = new BinaryReader(stream); |
||||||
|
var magic = Encoding.ASCII.GetString(reader.ReadBytes(4)); |
||||||
|
var version = reader.ReadInt32(); |
||||||
|
var compression = reader.ReadByte(); |
||||||
|
if (magic != "FrDT") throw new Exception("Invalid magic number"); |
||||||
|
if (version != 0) throw new Exception("Unknown version"); |
||||||
|
if (compression != 3) throw new Exception("Unsupported compression"); |
||||||
|
|
||||||
|
// Decompress and parse BSON. |
||||||
|
using var brotli = new BrotliStream(stream, CompressionMode.Decompress); |
||||||
|
return BsonReader.Load(brotli); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public record DataTreeNode(DataTree Tree, DataTreeNode? Parent, BsonValue Bson) |
||||||
|
{ |
||||||
|
public List<DataTreeNode> Children { get; } = []; |
||||||
|
|
||||||
|
public string ID => (string)Bson["ID"]; |
||||||
|
public string Name => (string)Bson["Name"]["Data"]; |
||||||
|
|
||||||
|
public Vector3 Position { get { var p = Bson["Position"]["Data"].ToArray<float>(); return new(p[0], p[1], p[2]); } } |
||||||
|
public Quaternion Rotation { get { var r = Bson["Rotation"]["Data"].ToArray<float>(); return new(r[0], r[1], r[2], r[3]); } } |
||||||
|
public Vector3 Scale { get { var s = Bson["Scale"] ["Data"].ToArray<float>(); return new(s[0], s[1], s[2]); } } |
||||||
|
|
||||||
|
public void Remove() { |
||||||
|
Tree.Nodes.Remove(ID); |
||||||
|
Parent?.Children.Remove(this); |
||||||
|
} |
||||||
|
|
||||||
|
public IEnumerable<DataTreeNode> EnumerateRecursive() |
||||||
|
{ |
||||||
|
yield return this; |
||||||
|
// Iterate children in reverse order so calling Remove on the current child is safe? |
||||||
|
// for (var i = Children.Count - 1; i >= 0; i--) |
||||||
|
foreach (var child in Children) |
||||||
|
foreach (var elem in child.EnumerateRecursive()) |
||||||
|
yield return elem; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
using System.Numerics; |
||||||
|
using Elements.Assets; |
||||||
|
using SharpGLTF.Geometry; |
||||||
|
using SharpGLTF.Geometry.VertexTypes; |
||||||
|
using SharpGLTF.Materials; |
||||||
|
using SharpGLTF.Schema2; |
||||||
|
|
||||||
|
namespace Res2tf; |
||||||
|
|
||||||
|
public class MeshAsset(MeshX meshX, ModelRoot model) |
||||||
|
{ |
||||||
|
public MeshX MeshX { get; } = meshX; |
||||||
|
public Mesh Mesh { get; } = model.CreateMesh(Build(meshX)); |
||||||
|
|
||||||
|
static IMeshBuilder<MaterialBuilder> Build(MeshX meshX) |
||||||
|
=> (meshX.HasNormals, meshX.HasTangents) switch { |
||||||
|
(false, false) => Build<VertexPosition>(meshX), |
||||||
|
( true, false) => Build<VertexPositionNormal>(meshX), |
||||||
|
( true, true) => Build<VertexPositionNormalTangent>(meshX), |
||||||
|
_ => throw new InvalidOperationException(), |
||||||
|
}; |
||||||
|
|
||||||
|
static IMeshBuilder<MaterialBuilder> Build<G>(MeshX meshX) |
||||||
|
where G : struct, IVertexGeometry |
||||||
|
=> (meshX.HasColors, meshX.UV_ChannelCount) switch { |
||||||
|
(false, 0) => Build<G, VertexEmpty>(meshX), |
||||||
|
(false, 1) => Build<G, VertexTexture1>(meshX), |
||||||
|
(false, 2) => Build<G, VertexTexture2>(meshX), |
||||||
|
(false, 3) => Build<G, VertexTexture3>(meshX), |
||||||
|
(false, 4) => Build<G, VertexTexture4>(meshX), |
||||||
|
( true, 0) => Build<G, VertexColor1>(meshX), |
||||||
|
( true, 1) => Build<G, VertexColor1Texture1>(meshX), |
||||||
|
( true, 2) => Build<G, VertexColor1Texture2>(meshX), |
||||||
|
( true, 3) => Build<G, VertexColor1Texture3>(meshX), |
||||||
|
( true, 4) => Build<G, VertexColor1Texture4>(meshX), |
||||||
|
_ => throw new InvalidOperationException(), |
||||||
|
}; |
||||||
|
|
||||||
|
static IMeshBuilder<MaterialBuilder> Build<G, M>(MeshX meshX) |
||||||
|
where G : struct, IVertexGeometry |
||||||
|
where M : struct, IVertexMaterial |
||||||
|
=> meshX.HasBoneBindings |
||||||
|
? Build<G, M, VertexJoints4>(meshX) |
||||||
|
: Build<G, M, VertexEmpty>(meshX); |
||||||
|
|
||||||
|
static MeshBuilder<G, M, S> Build<G, M, S>(MeshX meshX) |
||||||
|
where G : struct, IVertexGeometry |
||||||
|
where M : struct, IVertexMaterial |
||||||
|
where S : struct, IVertexSkinning |
||||||
|
{ |
||||||
|
var mesh = new MeshBuilder<G, M, S>(); |
||||||
|
|
||||||
|
foreach (var subMeshX in meshX.Submeshes) { |
||||||
|
var prim = mesh.UsePrimitive(new()); // placeholder materials |
||||||
|
|
||||||
|
VertexBuilder<G, M, S> BuildVertex(Vertex v) { |
||||||
|
var vertex = new VertexBuilder<G, M, S> { Position = v.Position }; |
||||||
|
if (meshX.HasNormals ) vertex.Geometry.SetNormal(v.Normal); |
||||||
|
if (meshX.HasTangents) vertex.Geometry.SetTangent(v.Tangent4); |
||||||
|
if (meshX.HasColors ) vertex.Material.SetColor(0, new(v.Color.r, v.Color.g, v.Color.b, v.Color.a)); |
||||||
|
for (var i = 0; i < meshX.UV_ChannelCount; i++) vertex.Material.SetTexCoord(i, v.GetUV(i)); |
||||||
|
if (meshX.HasBoneBindings) { var b = v.BoneBinding; vertex.Skinning.SetBindings( |
||||||
|
(b.boneIndex0, b.weight0), (b.boneIndex1, b.weight1), (b.boneIndex2, b.weight2), (b.boneIndex3, b.weight3)); } |
||||||
|
return vertex; |
||||||
|
} |
||||||
|
|
||||||
|
foreach (var t in subMeshX.RawIndicies.Chunk(3)) |
||||||
|
prim.AddTriangle(BuildVertex(meshX.GetVertex(t[0])), |
||||||
|
BuildVertex(meshX.GetVertex(t[1])), |
||||||
|
BuildVertex(meshX.GetVertex(t[2]))); |
||||||
|
} |
||||||
|
|
||||||
|
return mesh; |
||||||
|
} |
||||||
|
|
||||||
|
public IEnumerable<Matrix4x4> GetInverseBindMatrices() |
||||||
|
=> MeshX.Bones |
||||||
|
.Select(b => b.BindPose) |
||||||
|
.Select(ResoniteToNumerics) |
||||||
|
.Select(NormalizeIdentityColumn); |
||||||
|
|
||||||
|
|
||||||
|
static Matrix4x4 ResoniteToNumerics(Elements.Core.float4x4 m) |
||||||
|
=> new(m.m00, m.m10, m.m20, m.m30, |
||||||
|
m.m01, m.m11, m.m21, m.m31, |
||||||
|
m.m02, m.m12, m.m22, m.m32, |
||||||
|
m.m03, m.m13, m.m23, m.m33); |
||||||
|
|
||||||
|
// glTF is very sensitive about the identity column being exactly (0, 0, 0, 1). |
||||||
|
static Matrix4x4 NormalizeIdentityColumn(Matrix4x4 m) |
||||||
|
=> (IsApproxEqual(m.M14, 0) && IsApproxEqual(m.M24, 0) && IsApproxEqual(m.M34, 0) && IsApproxEqual(m.M44, 1)) |
||||||
|
? m with { M14 = 0, M24 = 0, M34 = 0, M44 = 1 } : m; |
||||||
|
|
||||||
|
static bool IsApproxEqual(float a, float b, float epsilon = 0.00001f) |
||||||
|
=> MathF.Abs(a - b) < epsilon; |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
This repository contains some code that was supposed to help me fix my avatar |
||||||
|
in [Resonite], which turned out to not be appropriate for public instances. |
||||||
|
|
||||||
|
 |
||||||
|
|
||||||
|
Unfortunately I have yet to actually make the round-trip back into Resonite |
||||||
|
work properly. Unsure if I'm mapping the bones correctly. |
||||||
|
|
||||||
|
### How it works |
||||||
|
|
||||||
|
Because Resonite does not (yet) allow "skinned" meshes (with bones and weights |
||||||
|
and all that stuff) to be exported in a way that keeps the skinning information, |
||||||
|
we have to go about it in a more roundabout way: |
||||||
|
|
||||||
|
- Export the model as a Resonite Package, place in `test/Test.resonitepackage` |
||||||
|
- Open the package, which is actually just disguised `.zip` archive |
||||||
|
- Parse the `R-Main.record` entry, a JSON file |
||||||
|
- The `assetUri` field points to the main asset file in the archive |
||||||
|
- Parse this file using [BSON], a horrible "Binary JSON" format |
||||||
|
- The resulting `DataTree` contains the hierarchical node and component info |
||||||
|
- Types are encoded as IDs, which we can decode using a lookup array |
||||||
|
- Process supported asset types: Meshes, textures, and materials |
||||||
|
- These assets can then be referenced by their ID by other nodes |
||||||
|
- Go through all node and attach meshes to them if they have some |
||||||
|
- Export the whole thing as a `.glb` with [SharpGLTF] |
||||||
|
|
||||||
|
**NOTE:** It might be possible to parse a good chunk of this with Resonite's |
||||||
|
own library, but it probably requires setting up an `Engine` or so for it to |
||||||
|
parse all the related files, I don't have the know-how to do that. |
||||||
|
|
||||||
|
[Resonite]: https://resonite.com/ |
||||||
|
[SharpGLTF]: https://github.com/vpenades/SharpGLTF |
||||||
|
[BSON]: https://bsonspec.org/ |
@ -0,0 +1,137 @@ |
|||||||
|
using System.Diagnostics.CodeAnalysis; |
||||||
|
using System.IO.Compression; |
||||||
|
using System.Numerics; |
||||||
|
using System.Text.Json; |
||||||
|
using Elements.Assets; |
||||||
|
using SharpGLTF.Materials; |
||||||
|
using SharpGLTF.Schema2; |
||||||
|
|
||||||
|
namespace Res2tf; |
||||||
|
|
||||||
|
public class ResonitePackage : IDisposable |
||||||
|
{ |
||||||
|
readonly ZipArchive _zip; |
||||||
|
|
||||||
|
public DataTree Tree { get; } |
||||||
|
public BsonValue Bson => Tree.Bson; |
||||||
|
public Dictionary<string, object> Assets { get; } = []; |
||||||
|
|
||||||
|
public ResonitePackage(string path) |
||||||
|
{ |
||||||
|
_zip = new ZipArchive(File.OpenRead(path)); |
||||||
|
|
||||||
|
using var mainRecord = _zip.GetEntry("R-Main.record")!.Open(); |
||||||
|
var mainDocument = JsonDocument.Parse(mainRecord); |
||||||
|
var mainAssetUri = mainDocument.RootElement.GetProperty("assetUri").GetString()!; |
||||||
|
|
||||||
|
Tree = new(ResolveToStream(mainAssetUri)); |
||||||
|
} |
||||||
|
|
||||||
|
public void Dispose() |
||||||
|
{ |
||||||
|
_zip.Dispose(); |
||||||
|
GC.SuppressFinalize(this); |
||||||
|
} |
||||||
|
|
||||||
|
public byte[] ResolveToBytes(string uriString) |
||||||
|
{ |
||||||
|
using var stream = ResolveToStream(uriString, true); |
||||||
|
return ((MemoryStream)stream).ToArray(); |
||||||
|
} |
||||||
|
public Stream ResolveToStream(string uriString, bool copy = false) |
||||||
|
{ |
||||||
|
// URIs coming from the DataTree are gonna start with an '@' symbol, so just strip it. |
||||||
|
if (uriString.StartsWith('@')) uriString = uriString[1..]; |
||||||
|
|
||||||
|
var uri = new Uri(uriString); |
||||||
|
if (uri.Scheme != "packdb") throw new ArgumentException("Not a packdb URI"); |
||||||
|
if (!uri.AbsolutePath.StartsWith('/')) throw new ArgumentException("Not valid packdb URI"); |
||||||
|
|
||||||
|
var stream = _zip.GetEntry($"Assets{uri.AbsolutePath}")!.Open(); |
||||||
|
if (!copy) return stream; |
||||||
|
else using (stream) { |
||||||
|
var bytes = new MemoryStream(); |
||||||
|
stream.CopyTo(bytes); |
||||||
|
bytes.Seek(0, SeekOrigin.Begin); |
||||||
|
return bytes; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void ProcessAssets(ModelRoot model) |
||||||
|
{ |
||||||
|
// Process static assets first. |
||||||
|
var staticMesh = Tree.GetTypeId("[FrooxEngine]FrooxEngine.StaticMesh"); |
||||||
|
var staticTexture = Tree.GetTypeId("[FrooxEngine]FrooxEngine.StaticTexture2D"); |
||||||
|
foreach (var asset in Bson["Assets"].AsList()) { |
||||||
|
var type = (int)asset["Type"]; |
||||||
|
|
||||||
|
if (type == staticMesh) { |
||||||
|
var id = (string)asset["Data"]["ID"]; |
||||||
|
var uri = (string)asset["Data"]["URL"]["Data"]; |
||||||
|
var mesh = new MeshAsset(new MeshX(ResolveToStream(uri, true)), model); |
||||||
|
Assets[id] = mesh; |
||||||
|
} |
||||||
|
|
||||||
|
else if (type == staticTexture) { |
||||||
|
var id = (string)asset["Data"]["ID"]; |
||||||
|
var uri = (string)asset["Data"]["URL"]["Data"]; |
||||||
|
// For now, texture "asset" is just its URI, material will load it from there. |
||||||
|
Assets[id] = uri; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var toonMaterial = Tree.GetTypeId("[FrooxEngine]FrooxEngine.XiexeToonMaterial"); |
||||||
|
var unlitMaterial = Tree.GetTypeId("[FrooxEngine]FrooxEngine.UnlitMaterial"); |
||||||
|
foreach (var asset in Bson["Assets"].AsList()) { |
||||||
|
var type = (int)asset["Type"]; |
||||||
|
|
||||||
|
if (type == toonMaterial) { |
||||||
|
var id = (string)asset["Data"]["ID"]; |
||||||
|
|
||||||
|
var color = ResoniteToColor(asset["Data"]["Color"]); |
||||||
|
|
||||||
|
var textureId = (string?)asset["Data"]["MainTexture"]["Data"]; |
||||||
|
var texturePath = (textureId != null) ? (string)Assets[textureId] : null; |
||||||
|
var texture = (texturePath != null) ? ResolveToBytes(texturePath) : null; |
||||||
|
|
||||||
|
var material = new MaterialBuilder() |
||||||
|
.WithDoubleSide(false) |
||||||
|
.WithBaseColor(color) |
||||||
|
.WithChannelImage(KnownChannel.BaseColor, texture); |
||||||
|
Assets[id] = material; |
||||||
|
} |
||||||
|
|
||||||
|
else if (type == unlitMaterial) { |
||||||
|
var id = (string)asset["Data"]["ID"]; |
||||||
|
|
||||||
|
var color = ResoniteToColor(asset["Data"]["TintColor"]); |
||||||
|
|
||||||
|
var textureId = (string)asset["Data"]["Texture"]["Data"]; |
||||||
|
var texturePath = (textureId != null) ? (string)Assets[textureId] : null; |
||||||
|
var texture = (texturePath != null) ? ResolveToBytes(texturePath) : null; |
||||||
|
|
||||||
|
var material = new MaterialBuilder() |
||||||
|
.WithDoubleSide(false) |
||||||
|
.WithBaseColor(color) |
||||||
|
.WithChannelImage(KnownChannel.BaseColor, texture); |
||||||
|
Assets[id] = material; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public bool TryGetAsset<TAsset>(string assetId, [NotNullWhen(true)] out TAsset? asset) |
||||||
|
{ |
||||||
|
if (Assets.TryGetValue(assetId, out var assetUntyped) && (assetUntyped is TAsset a)) |
||||||
|
{ asset = a; return true; } else { asset = default; return false; } |
||||||
|
} |
||||||
|
|
||||||
|
static Vector4 ResoniteToColor(BsonValue bson) |
||||||
|
{ |
||||||
|
var list = bson["Data"].AsList(); |
||||||
|
var r = Math.Clamp((float)list[0], 0.0f, 1.0f); |
||||||
|
var g = Math.Clamp((float)list[1], 0.0f, 1.0f); |
||||||
|
var b = Math.Clamp((float)list[2], 0.0f, 1.0f); |
||||||
|
var a = Math.Clamp((float)list[3], 0.0f, 1.0f); |
||||||
|
return new(r, g, b, a); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
This is free and unencumbered software released into the public domain. |
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or |
||||||
|
distribute this software, either in source code form or as a compiled |
||||||
|
binary, for any purpose, commercial or non-commercial, and by any |
||||||
|
means. |
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors |
||||||
|
of this software dedicate any and all copyright interest in the |
||||||
|
software to the public domain. We make this dedication for the benefit |
||||||
|
of the public at large and to the detriment of our heirs and |
||||||
|
successors. We intend this dedication to be an overt act of |
||||||
|
relinquishment in perpetuity of all present and future rights to this |
||||||
|
software under copyright law. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR |
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, |
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR |
||||||
|
OTHER DEALINGS IN THE SOFTWARE. |
||||||
|
|
||||||
|
For more information, please refer to <https://unlicense.org/> |
After Width: | Height: | Size: 587 KiB |
Loading…
Reference in new issue