Initial commit

main
copygirl 4 days ago
commit 49fee37cf5
  1. 4
      .gitignore
  2. 278
      Bson.cs
  3. 82
      DataTree.cs
  4. 96
      MeshAsset.cs
  5. 63
      Program.cs
  6. 33
      README.md
  7. 21
      Res2tf.csproj
  8. 137
      ResonitePackage.cs
  9. 24
      UNLICENSE.txt
  10. BIN
      docs/blender.png

4
.gitignore vendored

@ -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,63 @@
using Res2tf;
using SharpGLTF.Materials;
using SharpGLTF.Schema2;
using SharpGLTF.Transforms;
using var package = new ResonitePackage("test/Test.resonitepackage");
var nodeMap = new Dictionary<DataTreeNode, Node>();
var model = ModelRoot.CreateModel();
// Initialize Data on the DataTree nodes to scene nodes.
foreach (var n in package.Tree.Root.EnumerateRecursive()) {
if (n.Parent == null) continue; // Tree root => scene root.
var parent = nodeMap.TryGetValue(n.Parent, out var parentNode)
? (IVisualNodeContainer)parentNode : model.UseScene(0);
var node = parent.CreateNode(n.Name);
node.LocalTransform = new(n.Scale, n.Rotation, n.Position);
nodeMap[n] = node;
}
package.ProcessAssets(model);
var meshRenderer = package.Tree.GetTypeId("[FrooxEngine]FrooxEngine.MeshRenderer");
var skinnedMeshRenderer = package.Tree.GetTypeId("[FrooxEngine]FrooxEngine.SkinnedMeshRenderer");
// Add meshes to the Scene hierarchy.
// (Skinned meshes need skeleton bones for this to work.)
foreach (var node in package.Tree.Root.EnumerateRecursive())
foreach (var component in node.Bson["Components"]["Data"].AsList()) {
var type = (int)component["Type"];
if (type == meshRenderer) {
var assetId = (string)component["Data"]["Mesh"]["Data"];
if (package.TryGetAsset<MeshAsset>(assetId, out var asset)) {
var materials = component["Data"]["Materials"]["Data"].AsList()
.Select(material => (MaterialBuilder)package.Assets[(string)material["Data"]]);
foreach (var (i, material) in materials.Select((m, i) => (i, m)))
asset.Mesh.Primitives[i].Material = model.CreateMaterial(material);
nodeMap[node].WithMesh(asset.Mesh);
}
} else if (type == skinnedMeshRenderer) {
var assetId = (string)component["Data"]["Mesh"]["Data"];
if (package.TryGetAsset<MeshAsset>(assetId, out var asset)) {
var materials = component["Data"]["Materials"]["Data"].AsList()
.Select(material => (MaterialBuilder)package.Assets[(string)material["Data"]]);
foreach (var (i, material) in materials.Select((m, i) => (i, m)))
asset.Mesh.Primitives[i].Material = model.CreateMaterial(material);
var bones = component["Data"]["Bones"]["Data"].AsList()
.Select(bone => nodeMap[package.Tree.Nodes[(string)bone["Data"]]]);
var inverseBoneMatrices = asset.GetInverseBindMatrices();
var joints = bones.Zip(inverseBoneMatrices).ToArray();
// Skeleton node must not have transform.
nodeMap[node].LocalTransform = AffineTransform.Identity;
nodeMap[node].WithSkinnedMesh(asset.Mesh, joints);
}
}
}
model.SaveGLB("test/scene.glb");

@ -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.
![output viewed in Blender](docs/blender.png)
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,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="/home/copygirl/.local/share/Steam/steamapps/common/Resonite/Elements.Core.dll" />
<Reference Include="/home/copygirl/.local/share/Steam/steamapps/common/Resonite/Elements.Assets.dll" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SharpGLTF.Core" Version="1.0.4" />
<PackageReference Include="SharpGLTF.Runtime" Version="1.0.4" />
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.4" />
</ItemGroup>
</Project>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Loading…
Cancel
Save