GAMES 6320 · Assignment 06

A human-readable mesh format and a MeshBuilder

A human-readable mesh file format, a MeshBuilder tool, and loading meshes from disk.

7 min readC++ · Direct3D · OpenGL

Course: GAMES 6320-001 — Game Engineering II (Fall 2026)

Download (one click, ZIP): MyGame_Assignment06_Release_x64.zip

The ZIP contains a Direct3D Release build (x64). Unzip it and run MyGame.exe.

Controls:

Key What it does
Arrow keys Move the colored object left / right / up / down
Space (hold) The colored object becomes a triangle instead of a square
A / D Move the camera left / right
W / S Move the camera forward / back
Esc Exit

1. What the assignment was about

Until last week every mesh in my game was an array of numbers typed into cMyGame.cpp.
Changing the shape of anything meant changing C++ code and recompiling the game. This
assignment moves geometry out of the code and into asset files:

  • Every mesh is now a file in the game’s Content/Meshes folder, written in a format that I
    designed.
  • The asset build system has a new builder, MeshBuilder, that “builds” those files into the
    game’s data/ folder.
  • The engine’s mesh factory function no longer takes vertex and index arrays; it takes the
    path of a mesh file, reads it, and creates the GPU buffers from what it read.

The file format is Lua, which my engine already uses for its build scripts. A Lua file that
returns a table is a very convenient data format: it has arrays, dictionaries, numbers,
strings and comments, and the engine already has a Lua library to read it.


2. Why human-readable asset files?

  • You can open them and see what is wrong. When something renders incorrectly the first
    question is “is the data wrong or the code wrong?”. With a text file I can open the mesh and
    read the positions and triangles directly, without writing a tool first.
  • Small changes don’t need a programmer or a recompile. Moving one vertex is editing one
    number and rebuilding the assets. The game’s code doesn’t change at all.
  • They work with source control. A diff of a text file shows exactly which vertex changed.
    A diff of a binary file only says “the file changed”.
  • The format can grow. Because data is labelled by name, a newer version of the format can
    add things (vertex colors, normals) and old files keep working.

The cost is that text is bigger and slower to parse than binary. That doesn’t matter for a
source file, because the plan is to convert it into a compact binary file at build time.
The human-readable file is for people, and the built file will be for the game.


3. My mesh file format

This is house.mesh, the white house in the screenshot:

--[[
A simple house: a square body with a triangular roof,
centered on its local origin
]]

return
{
-- Each vertex is a table of named values
-- (currently a vertex only has a position, but more data can be added by name)
vertices =
{
-- Body
-- [0] bottom left
{ position = { x = -0.4, y = -0.5, z = 0.0 } },
-- [1] bottom right
{ position = { x = 0.4, y = -0.5, z = 0.0 } },
-- [2] top right of the walls
{ position = { x = 0.4, y = 0.1, z = 0.0 } },
-- [3] top left of the walls
{ position = { x = -0.4, y = 0.1, z = 0.0 } },

-- Roof
-- [4] left eave
{ position = { x = -0.55, y = 0.1, z = 0.0 } },
-- [5] right eave
{ position = { x = 0.55, y = 0.1, z = 0.0 } },
-- [6] peak
{ position = { x = 0.0, y = 0.5, z = 0.0 } },
},

-- Each triangle lists three vertices (by their index in the vertices table above, starting at 0)
-- in counter-clockwise order when looking at the front of the triangle
triangles =
{
-- Body
{ 0, 1, 2 },
{ 0, 2, 3 },
-- Roof
{ 4, 5, 6 },
},
}

Why it looks like this

For every piece of data I asked the question from the lecture: does the order matter?

Data Order matters? So it is a…
The top-level table (vertices, triangles) No dictionary
The list of vertices Yes — triangles refer to vertices by their position in the list array
One vertex No — a vertex is a bag of attributes dictionary (position = …)
A position Not really (x, y, z) dictionary (x =, y =, z =)
The list of triangles Arbitrary, but there must be some order array
One triangle Yes — the order of its three indices is the winding order array of 3

Some specific decisions:

  • A vertex is a dictionary, even though it only has one entry. I could have written a vertex
    as just { -0.4, -0.5, 0.0 }. It is shorter, but it only works as long as a vertex is nothing
    but a position. Next week vertices get a color; with my format that is a new named entry
    (color = { … }) next to position, and files without a color can still be read.
  • x, y and z are named. The lecture said an array would also be fine here, because anyone who
    knows a little graphics reads { -0.4, -0.5, 0.0 } as x, y, z. I chose names anyway because the
    file is meant to be read by people who are debugging, and y = 0.5 can’t be misread.
  • Indices are grouped into triangles. A flat list 0, 1, 2, 0, 2, 3, 4, 5, 6 is what the GPU
    wants, but it hides where one triangle ends. Grouping by three makes every line one triangle,
    and adding or removing a triangle is adding or removing one line.
  • Indices start at 0. Lua arrays start at 1, so I had to choose. I went with 0 because that is
    what the engine and the GPU use, and the file states it explicitly in a comment. The loader
    does the conversion.
  • No counts. There is no vertexCount or triangleCount: the loader counts the entries.
    A count in the file would be redundant data that can be wrong, and then the file
    contradicts itself.
  • Comments carry the helpful but optional information. The -- [4] left eave comments give
    each vertex its index and a name. That is exactly the kind of information that helps a person
    but that the format must not require. Lua comments are ignored by the machine, so a person
    (or, later, an exporter) can add as many as they like.
  • The winding order is fixed and documented. Triangles are always counter-clockwise
    (right-handed), and the engine reverses them for Direct3D when it loads the mesh. Callers never
    have to think about which platform they are on.

The extension is .mesh. The built file currently has the same name, because MeshBuilder
simply copies the file; when it starts writing a binary format the built file can get a
different extension without the game noticing anything besides a path change.


4. Building and loading mesh files

MeshBuilder is a new console application in Tools/. Its Build() function copies the source
file to the target path using the engine’s platform-independent Platform::CopyFile(), overwrites
an existing target, updates the target’s file time so the build system knows that it is up to
date, and reports any failure with OutputErrorMessageWithFileInfo() so that it shows up in Visual
Studio’s Error List.

AssetBuildFunctions.lua registers the new asset type:

NewAssetTypeInfo( "meshes",
{
GetBuilderRelativePath = function()
return "MeshBuilder.exe"
end,
}
)

and the game lists its meshes in AssetsToBuild.lua:

meshes =
{
"Meshes/square.mesh",
"Meshes/triangle.mesh",
"Meshes/house.mesh",
},

BuildMyGameAssets runs MeshBuilder.exe, so I made it depend on the MeshBuilder project. (It is
not a reference: BuildMyGameAssets doesn’t link with MeshBuilder; it just needs the program to
exist before it can build.)

The game now loads its meshes like this:

if ( !( result = Graphics::cMesh::Load( s_path_mesh_house, m_mesh_house ) ) )
{
EAE6320_ASSERTF( false, "Can't initialize MyGame without the house mesh" );
return result;
}

Inside cMesh::Load() I read the file with one small function per table level
(LoadVertices(), LoadVertex(), LoadPosition(), LoadTriangles(), LoadTriangle()). Each one
starts with a comment saying what is on top of the Lua stack, and each one pops exactly what it
pushed. That was the single most useful habit for this assignment: Lua stack bugs are invisible
in the debugger, so making every function responsible for one level kept the stack easy to
reason about.

The loader is strict and specific about errors. A missing position, an index that refers to a
vertex that doesn’t exist, a triangle with two indices, or a file that doesn’t return a table
all produce a log message that names the file and the vertex or triangle, and the game then
exits with its normal error message instead of rendering garbage.


5. Screenshots

The game looks the same as last week on purpose, except that the white landmark is now a house
that comes from house.mesh:

The game running

As a test I deleted the roof’s three indices { 4, 5, 6 } from house.mesh, rebuilt only the
assets, and ran the game without changing any code. The roof is gone:

The house without its roof


6. Debugging MeshBuilder

To debug MeshBuilder I set it as the startup project and gave it the same command arguments that
the asset build passes to it (the source path of a mesh and its target path in the game’s data
folder). Here it is stopped in cMeshBuilder::Build(), just before the copy, with the source and
target paths visible in the Autos window:

Debugging MeshBuilder in Visual Studio


7. Build / test

All four configurations (Debug|x64, Release|x64 for Direct3D and Debug|x86, Release|x86 for
OpenGL) build from scratch without errors or warnings, and both platforms render the same scene.
Building only the game project and running it shows the “Initialization failed” message and exits;
after building only BuildMyGameAssets the game runs.


8. Reflection

The design of the file format took longer than the code. My first version was very compact —
vertices as { x, y, z } arrays and one flat index list — and it was easy to write but hard to
check: to find the roof I had to count indices in threes. Grouping indices into triangles and
naming everything made the file longer, but now I can read a triangle and see which vertices it
uses without counting anything.

Thinking about next week while designing helped. Making each vertex a dictionary costs a few
characters per line today, but it means adding a vertex color will not break a single existing
mesh file.

Strict validation in the loader paid off immediately: when I typed a wrong field name while
testing, the log said exactly which vertex in which file was wrong, instead of the mesh just not
showing up.

What I would change in a real engine: parsing Lua at run time is the slowest possible way to load
geometry. The right place for all of this code is MeshBuilder, which should read the
human-readable file once at build time and write a binary file that the game can load with
almost no work — including doing the Direct3D winding-order swap there instead of every time the
game starts.