using AssetRipper.Assets; using AssetRipper.Assets.Collections; using AssetRipper.Assets.Generics; using AssetRipper.Export.Modules.Textures; using AssetRipper.Numerics; using AssetRipper.Primitives; using AssetRipper.SourceGenerated.Classes.ClassID_1; using AssetRipper.SourceGenerated.Classes.ClassID_18; using AssetRipper.SourceGenerated.Classes.ClassID_2; using AssetRipper.SourceGenerated.Classes.ClassID_21; using AssetRipper.SourceGenerated.Classes.ClassID_25; using AssetRipper.SourceGenerated.Classes.ClassID_28; using AssetRipper.SourceGenerated.Classes.ClassID_33; using AssetRipper.SourceGenerated.Classes.ClassID_4; using AssetRipper.SourceGenerated.Classes.ClassID_43; using AssetRipper.SourceGenerated.Extensions; using AssetRipper.SourceGenerated.Subclasses.PPtr_Material; using AssetRipper.SourceGenerated.Subclasses.SubMesh; using AssetRipper.SourceGenerated.Subclasses.UnityTexEnv; using SharpGLTF.Geometry; using SharpGLTF.Materials; using SharpGLTF.Memory; using SharpGLTF.Scenes; using System.Buffers; namespace AssetRipper.Export.Modules.Models; public static class GlbLevelBuilder { public static SceneBuilder Build(IEnumerable assets, bool isScene) { SceneBuilder sceneBuilder = new(); BuildParameters parameters = new BuildParameters(isScene); HashSet exportedAssets = new(); foreach (IUnityObjectBase asset in assets) { if (!exportedAssets.Contains(asset) && asset is IGameObject or IComponent) { IGameObject root = GetRoot(asset); AddGameObjectToScene(sceneBuilder, parameters, null, Transformation.Identity, Transformation.Identity, root.GetTransform()); foreach (IEditorExtension exportedAsset in root.FetchHierarchy()) { exportedAssets.Add(exportedAsset); } } } return sceneBuilder; } private static void AddGameObjectToScene(SceneBuilder sceneBuilder, BuildParameters parameters, NodeBuilder? parentNode, Transformation parentGlobalTransform, Transformation parentGlobalInverseTransform, ITransform transform) { IGameObject? gameObject = transform.GameObject_C4P; if (gameObject is null) { return; } Transformation localTransform = transform.ToTransformation(); Transformation localInverseTransform = transform.ToInverseTransformation(); Transformation globalTransform = localTransform * parentGlobalTransform; Transformation globalInverseTransform = parentGlobalInverseTransform * localInverseTransform; NodeBuilder node = parentNode is null ? new NodeBuilder(gameObject.Name) : parentNode.CreateNode(gameObject.Name); if (parentNode is not null || parameters.IsScene) { node.LocalTransform = new SharpGLTF.Transforms.AffineTransform( transform.LocalScale_C4.CastToStruct(),//Scaling is the same in both coordinate systems GlbCoordinateConversion.ToGltfQuaternionConvert(transform.LocalRotation_C4), GlbCoordinateConversion.ToGltfVector3Convert(transform.LocalPosition_C4)); } sceneBuilder.AddNode(node); if (gameObject.TryGetComponent(out IMeshFilter? meshFilter) && meshFilter.TryGetMesh(out IMesh? mesh) && mesh.IsSet() && parameters.TryGetOrMakeMeshData(mesh, out MeshData meshData)) { if (gameObject.TryGetComponent(out IRenderer? meshRenderer)) { if (ReferencesDynamicMesh(meshRenderer)) { AddDynamicMeshToScene(sceneBuilder, parameters, node, mesh, meshData, new MaterialList(meshRenderer)); } else { int[] subsetIndices = GetSubsetIndices(meshRenderer); AddStaticMeshToScene(sceneBuilder, parameters, node, mesh, meshData, subsetIndices, new MaterialList(meshRenderer), globalTransform, globalInverseTransform); } } } foreach (ITransform childTransform in transform.Children_C4P.WhereNotNull()) { AddGameObjectToScene(sceneBuilder, parameters, node, localTransform * parentGlobalTransform, parentGlobalInverseTransform * localInverseTransform, childTransform); } } private static void AddDynamicMeshToScene(SceneBuilder sceneBuilder, BuildParameters parameters, NodeBuilder node, IMesh mesh, MeshData meshData, MaterialList materialList) { AccessListBase subMeshes = mesh.SubMeshes; (ISubMesh, MaterialBuilder)[] subMeshArray = ArrayPool<(ISubMesh, MaterialBuilder)>.Shared.Rent(subMeshes.Count); for (int i = 0; i < subMeshes.Count; i++) { MaterialBuilder materialBuilder = parameters.GetOrMakeMaterial(materialList[i]); subMeshArray[i] = (subMeshes[i], materialBuilder); } ArraySegment<(ISubMesh, MaterialBuilder)> arraySegment = new ArraySegment<(ISubMesh, MaterialBuilder)>(subMeshArray, 0, subMeshes.Count); IMeshBuilder subMeshBuilder = GlbSubMeshBuilder.BuildSubMeshes(arraySegment, mesh.Is16BitIndices(), meshData, Transformation.Identity, Transformation.Identity); sceneBuilder.AddRigidMesh(subMeshBuilder, node); ArrayPool<(ISubMesh, MaterialBuilder)>.Shared.Return(subMeshArray); } private static void AddStaticMeshToScene(SceneBuilder sceneBuilder, BuildParameters parameters, NodeBuilder node, IMesh mesh, MeshData meshData, int[] subsetIndices, MaterialList materialList, Transformation globalTransform, Transformation globalInverseTransform) { (ISubMesh, MaterialBuilder)[] subMeshArray = ArrayPool<(ISubMesh, MaterialBuilder)>.Shared.Rent(subsetIndices.Length); AccessListBase subMeshes = mesh.SubMeshes; for (int i = 0; i < subsetIndices.Length; i++) { ISubMesh subMesh = subMeshes[subsetIndices[i]]; MaterialBuilder materialBuilder = parameters.GetOrMakeMaterial(materialList[i]); subMeshArray[i] = (subMesh, materialBuilder); } ArraySegment<(ISubMesh, MaterialBuilder)> arraySegment = new ArraySegment<(ISubMesh, MaterialBuilder)>(subMeshArray, 0, subsetIndices.Length); IMeshBuilder subMeshBuilder = GlbSubMeshBuilder.BuildSubMeshes(arraySegment, mesh.Is16BitIndices(), meshData, globalInverseTransform, globalTransform); sceneBuilder.AddRigidMesh(subMeshBuilder, node); ArrayPool<(ISubMesh, MaterialBuilder)>.Shared.Return(subMeshArray); } private static IGameObject GetRoot(IUnityObjectBase asset) { return asset switch { IGameObject gameObject => gameObject.GetRoot(), IComponent component => component.GameObject_C2P!.GetRoot(), _ => throw new InvalidOperationException() }; } private static bool ReferencesDynamicMesh(IRenderer renderer) { return renderer.Has_StaticBatchInfo_C25() && renderer.StaticBatchInfo_C25.SubMeshCount == 0 || renderer.Has_SubsetIndices_C25() && renderer.SubsetIndices_C25.Count == 0; } private static int[] GetSubsetIndices(IRenderer renderer) { AccessListBase materials = renderer.Materials_C25; if (renderer.Has_SubsetIndices_C25()) { return renderer.SubsetIndices_C25.Select(i => (int)i).ToArray(); } else if (renderer.Has_StaticBatchInfo_C25()) { return Enumerable.Range(renderer.StaticBatchInfo_C25.FirstSubMesh, renderer.StaticBatchInfo_C25.SubMeshCount).ToArray(); } else { return Array.Empty(); } } private readonly record struct BuildParameters( MaterialBuilder DefaultMaterial, Dictionary ImageCache, Dictionary MaterialCache, Dictionary MeshCache, bool IsScene) { public BuildParameters(bool isScene) : this(new MaterialBuilder("DefaultMaterial"), new(), new(), new(), isScene) { } public bool TryGetOrMakeMeshData(IMesh mesh, out MeshData meshData) { if (MeshCache.TryGetValue(mesh, out meshData)) { return true; } else if (MeshData.TryMakeFromMesh(mesh, out meshData)) { MeshCache.Add(mesh, meshData); return true; } return false; } public MaterialBuilder GetOrMakeMaterial(IMaterial? material) { if (material is null) { return DefaultMaterial; } if (!MaterialCache.TryGetValue(material, out MaterialBuilder? materialBuilder)) { materialBuilder = MakeMaterialBuilder(material); MaterialCache.Add(material, materialBuilder); } return materialBuilder; } public bool TryGetOrMakeImage(ITexture2D texture, out MemoryImage image) { if (!ImageCache.TryGetValue(texture, out image)) { if (TextureConverter.TryConvertToBitmap(texture, out DirectBitmap bitmap)) { using MemoryStream memoryStream = new(); bitmap.SaveAsPng(memoryStream); image = new MemoryImage(memoryStream.ToArray()); ImageCache.Add(texture, image); return true; } return false; } else { return true; } } private MaterialBuilder MakeMaterialBuilder(IMaterial material) { MaterialBuilder materialBuilder = new MaterialBuilder(material.Name); GetTextures(material, out ITexture2D? mainTexture, out ITexture2D? normalTexture); if (mainTexture is not null && TryGetOrMakeImage(mainTexture, out MemoryImage mainImage)) { materialBuilder.WithBaseColor(mainImage); } if (normalTexture is not null && TryGetOrMakeImage(normalTexture, out MemoryImage normalImage)) { materialBuilder.WithNormal(normalImage); } return materialBuilder; } private static void GetTextures(IMaterial material, out ITexture2D? mainTexture, out ITexture2D? normalTexture) { mainTexture = null; normalTexture = null; ITexture2D? mainReplacement = null; foreach ((Utf8String utf8Name, IUnityTexEnv textureParameter) in material.GetTextureProperties()) { string name = utf8Name.String; if (IsMainTexture(name)) { mainTexture ??= textureParameter.Texture.TryGetAsset(material.Collection) as ITexture2D; } else if (IsNormalTexture(name)) { normalTexture ??= textureParameter.Texture.TryGetAsset(material.Collection) as ITexture2D; } else { mainReplacement ??= textureParameter.Texture.TryGetAsset(material.Collection) as ITexture2D; } } mainTexture ??= mainReplacement; } private static bool IsMainTexture(string textureName) { return textureName is "_MainTex" or "texture" or "Texture" or "_Texture"; } private static bool IsNormalTexture(string textureName) { return textureName is "_Normal" or "Normal" or "normal"; } } private readonly struct MaterialList { private readonly AccessListBase materials; private readonly AssetCollection file; private MaterialList(AccessListBase materials, AssetCollection file) { this.materials = materials; this.file = file; } public MaterialList(IRenderer renderer) : this(renderer.Materials_C25, renderer.Collection) { } public int Count => materials.Count; public IMaterial? this[int index] { get { if (index >= materials.Count) { return null; } return materials[index].TryGetAsset(file); } } } }