0

I'm reverse engineering the GameCube game Zelda: Twilight Princess and recreating it in Unity. Specifically, I'm parsing binary files that contain data for meshes, textures, and materials to generate Unity GameObjects with meshes and materials.

My current implementation works, but it's kinda slow. One simple area takes about 100ms. When also creating all the npc's, objects and so on, it takes up to 2 seconds. When starting the game, I load all those binary files, while that my CPU usage is pretty low during the loading process (like 3% or something). I’m not sure how to structure the tasks better.

Here’s a simplified version of my code structure:

Load the binary file and parse the headers.

  • For each "chunk" (tag) in the file:
  • Decode the tag (e.g., vertices, textures, materials).
  • Store the parsed data in corresponding objects (e.g., INF1, SHP1, etc.).
  • After all chunks are parsed, create meshes and assign materials in Unity.

I hope somebody could help me!

Here’s the core of my current implementation (stripped down for brevity):

public class BMD : MonoBehaviour
{
    public string FullBMD = "";
    
    public INF1 INF1Tag;
    public VTX1 VTX1Tag;
    public EVP1 EVP1Tag;
    public DRW1 DRW1Tag;
    public JNT1 JNT1Tag;
    public SHP1 SHP1Tag;
    public MAT3 MAT3Tag;
    public TEX1 TEX1Tag;

    public Material DefaultMaterial;

    // This method takes 90ms to execute, so a faster loading process is required
    void Start()
    {
        using (EndianBinaryReader reader = new EndianBinaryReader(File.ReadAllBytes(FullBMD), Endian.Big))
        {
            reader.Skip(8);

            int size = reader.ReadInt32();
            int numChunks = reader.ReadInt32();
            
            reader.Skip(16);
            
            for (int i = 0; i < numChunks; i++)
            {
                long tagStart = reader.BaseStream.Position;

                string tagName = reader.ReadString(4);
                int tagSize = reader.ReadInt32();
                
                switch (tagName)
                {
                    case "INF1":
                        INF1Tag = new INF1();
                        INF1Tag.LoadINF1FromStream(reader, tagStart);
                        break;
                    case "VTX1":
                        VTX1Tag = new VTX1();
                        VTX1Tag.LoadVTX1FromStream(reader, tagStart, tagSize);
                        break;
                    case "EVP1":
                        EVP1Tag = new EVP1();
                        EVP1Tag.LoadEVP1FromStream(reader, tagStart);
                        break;
                    case "DRW1":
                        DRW1Tag = new DRW1();
                        DRW1Tag.LoadDRW1FromStream(reader, tagStart);
                        break;
                    case "JNT1":
                        JNT1Tag = new JNT1();
                        JNT1Tag.LoadJNT1FromStream(reader, tagStart);
                        JNT1Tag.CalculateParentJointsForSkeleton(INF1Tag.HierarchyRoot);
                        break;
                    case "SHP1":
                        SHP1Tag = new SHP1();
                        SHP1Tag.ReadSHP1FromStream(reader, tagStart, VTX1Tag.VertexData);
                        break;
                    case "MAT3":
                        MAT3Tag = new MAT3();
                        MAT3Tag.LoadMAT3FromStream(reader, tagStart);
                        break;
                    case "TEX1":
                        TEX1Tag = new TEX1();
                        TEX1Tag.LoadTEX1FromStream(this, reader, tagStart, new List<BTI>());
                        break;
                    case "MDL3":
                        break;
                }
                reader.BaseStream.Position = tagStart + tagSize;
            }
            // After loading data, create the meshes
            CreateMeshes();
        }
    }

    // This method takes 10ms to execute
    private void CreateMeshes()
    {
        List<MeshFilter> meshFilters = new List<MeshFilter>();
        List<MeshRenderer> meshRenderers = new List<MeshRenderer>();
        List<Material> meshMaterials = new List<Material>();
        List<GameObject> childs = new List<GameObject>();
        foreach (SHP1.Shape shape in SHP1Tag.Shapes)
        {
            GameObject go = new GameObject("shape" + SHP1Tag.Shapes.IndexOf(shape));
            childs.Add(go);
            
            Material3 material = MAT3Tag.MaterialList[MAT3Tag.MaterialRemapTable[shape.MaterialIndex]];
            BTI texture = TEX1Tag.BTIs[MAT3Tag.TextureRemapTable[baseTexture]];

            List<Vector3> verts = shape.OverrideVertPos.Count > 0 ? shape.OverrideVertPos : shape.VertexData.Position;
            List<Vector3> normals = shape.OverrideNormals.Count > 0 ? shape.OverrideNormals : shape.VertexData.Normal;
            
            // Create mesh
            Mesh mesh = new Mesh();
            mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
            mesh.vertices = verts.ToArray();

            int numVertices = verts.Count;
            int numTriangles = numVertices / 3;

            int[] triangles = new int[numTriangles * 3];            // Single sided
            for (int i = 0; i < numTriangles; i++)
            {
                triangles[i * 3] = i * 3;
                triangles[i * 3 + 1] = i * 3 + 1;
                triangles[i * 3 + 2] = i * 3 + 2;
            }
            mesh.triangles = triangles;
            mesh.normals = normals.ToArray();
            mesh.uv = shape.VertexData.Tex0.ToArray();
            mesh.colors = shape.VertexData.Color0.ToArray();
            
            // Assign material to shader
            Material mat = new Material(DefaultMaterial);
            mat.mainTexture = texture.Texture;
            
            MeshFilter filter = go.AddComponent<MeshFilter>();
            filter.sharedMesh = mesh;

            MeshRenderer renderer = go.AddComponent<MeshRenderer>();
            renderer.sharedMaterial = mat;
            
            meshFilters.Add(filter);
            meshRenderers.Add(renderer);
            meshMaterials.Add(mat);
        }

        // Combine meshes
        CombineMeshesToASingle();
    }
}
8
  • You could at least use a background thread to process most of these things while maintaining the UI main thread interactive (e.g. showing a loading bar to the user) instead of having a complete freeze. Then when it comes to actually create the meshes and types that require to be created in the Unity main thread you could use my Stopwatch approach from here in order to spread the load over multiple frames allowing a certain frame rate for user feedback - takes longer overall of course but is better UX Commented Dec 27, 2024 at 16:54
  • Some of these things can further also be done asynchronous in Unity e.g. using the Mesh API Commented Dec 27, 2024 at 16:55
  • 2
    If the CPU is not much used, then it is probably the storage device which limits the operation? I advise you to profile IO/s to be sure. On SSDs, asynchronous operation can help not to be latency-bound. Alternatively, You can also move IO operations in multiple separate dedicated threads. This is a significant work but big games often does that so to avoid slowing down a lot the main thread (IO/s operations have a huge latency, especially on HDDs). Commented Dec 27, 2024 at 16:57
  • Can you give us some idea of how much data you're loading? A few megabytes? A gigabyte? Commented Dec 27, 2024 at 19:07
  • 1
    Note that this may be a better fit for code review StackExchange, the rough rule of thumb being that SO is for broken code, CRSE is for working code (that needs some improvement) Commented Dec 31, 2024 at 14:58

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.