Every 3D model in this game — all 21,864 of them — had been drawing its own hidden interior. Into every frame, into every shadow, for the entire life of the project, because of a flag nobody set and nothing was looking at.
It surfaced as something much smaller. A goblin tower came down and the shards it shattered into came out as flat dark navy blocks with no lighting on them at all, and only sometimes. That is the whole bug report, and the last section of this post explains why those two facts are the same fact.

A 3D model is a shell of flat triangles, and every triangle has two sides: the outside, which you can see, and the inside, which you cannot because the outside is in the way. Skipping the hidden ones is called backface culling, it is a single flag on a material, and graphics cards have done it as a matter of course for thirty years. In this game they had not been, once, ever.
The flag is per material and it has three settings: draw the front only (the default in most engines and the right answer for anything solid), draw the back only, or draw both. This library shipped 21,864 models on the third setting.
Three files are named repeatedly below, so: glTF is the standard file format for 3D models on the web, and .glb is its single-file binary flavour — the format image-to-3D services return and the one three.js loads natively. Usefully for this post, every glTF file records the name and version of the tool that wrote it.
A flag set on everything carries no intent
Scanning the model library found that 21,864 files out of 21,864 carried at least one material marked as two sided. That is 100%, and a field carrying one value across an entire library was never chosen deliberately.
The next question is where it entered, and the files answer that themselves, because every glTF file records the name and version of whatever wrote it. Reading that field across the whole library turns up seven different writers: some belong to the AI service that generated most of these models, and the rest are the steps this project runs afterwards to slim a model down and repack it. Blender is one of those later steps, and the files it produced here came out two sided as well, because the files handed to it already were.
All seven wrote every material two sided. The untouched downloads are two sided before anything in this project opens them, so the flag is not something the pipeline here introduced. It is something the pipeline inherited and passed along, unexamined, at every step, because nothing in the chain was ever asking about it.
Blender's own default is the one most people meet first, and it works the same way: its glTF exporter marks a material two sided whenever the Backface Culling checkbox is unticked, and unticked is what you get if you never touch it. Any pipeline with a Blender step in it inherits that on every material. Useful, and not the cause here. The flag had already arrived.
Counting it took a throwaway script that never decodes any geometry at all. These files are a small header followed by a run of chunks, and the first chunk is plain JSON describing the materials, along with the name of the tool that wrote them. Read that, count the flag, ignore the rest of the file, and twenty thousand models go past in a few seconds. The result is not "these models are wrong". It is that the field carries no information, because it has the same value everywhere, no matter who wrote it.
What drawing both sides costs, and what it does not
Drawing both sides means the graphics card shades the interior surface of every closed model in the scene. Those pixels are almost always painted over by the outside surface a moment later, and on a solid object they contribute nothing to the picture at all.
The shadows were paying twice as well. Three.js works out which side to use for the shadow pass from the visible side, so a single-sided material draws its shadow from back faces only, and a two-sided one draws both. Every shadow-casting model in the game was drawing two shells into the shadow map.
What this does not buy is a single draw call. Hiding back faces happens inside a request that has already been issued, so the number of requests is identical before and after. That matters here, because draw calls turned out to be the thing that predicted frame time on this game and triangle count turned out not to be. This change moves a different lever entirely: it removes shading work being done for a picture it could never contribute to.
The risk was real, so it got measured rather than assumed
Hiding back faces is only free on closed shapes. Do it to a single flat sheet and the sheet becomes invisible from one side, which is exactly what a cape, a banner or a card of leaves is. Worse, a model whose triangles were accidentally turned inside out somewhere in its import chain disappears entirely, and generated models get turned inside out more often than hand-modelled ones do.
Two measurements decided it, and the first one transfers to any model library.
How open is this model, really? Join up every vertex that sits at the same point in space, then count the edges used by only one triangle and divide by the total number of edges. A closed solid scores zero, because every edge is shared by two triangles. An open sheet scores high, because its whole perimeter is an edge with nothing on the other side. Calibrate it on shapes whose answer you already know before you trust it on shapes you do not: a flat square made of two triangles scores 0.800, and a tetrahedron scores 0.000. Across the 633 models the game actually draws, the worst score is 1.4%. Across 1,123 more, including 584 dungeon props, the worst is 2.58%.
The joining-up step is the part that cannot be skipped. These files split a vertex in two wherever a texture seam or a hard edge needs it split, so a perfectly closed model looks like shredded paper if you count edges naively. Join by position first, then count. And sort the results descending and read the top of the list rather than an average: one cape among a thousand solids vanishes into a mean and is obvious as the first row.
The second measurement asked which materials were meant to be seen through at all, on the theory that anything transparent is a candidate for being a sheet. Out of 20,039 materials, four were, and all four sit inside animation files whose contents are never drawn.
There are no capes, banners or foliage cards in this game's roster. That is a fact about this specific art direction rather than a general licence, and it is the entire reason the change was allowed to ship.

One loop, and the guard that makes it safe
The fix runs once, when a model is first loaded and prepared, before the game starts making copies of it. In most three.js projects that is whatever wraps the loader and caches the result.
// Force single-sided, but ONLY where the exporter's default put it.
scene.traverse((obj) => {
for (const m of [].concat(obj.material ?? [])) {
if (m.side !== THREE.DoubleSide) continue; // a deliberate BackSide must survive
m.side = THREE.FrontSide;
m.needsUpdate = true;
}
});That continue is the load-bearing line. Only the two-sided default is rewritten, so anything deliberately set to draw its inside survives untouched, and this game has a real one. The comic-book outline around each unit is an inside-out shell sitting just outside the model, and it is added to each copy rather than to the master, so this pass never sees it in the first place. The outline rides along at every distance, and rewriting its shells would have flattened every outline in the game in a single frame.
The other exemption runs the other way. When a building is destroyed it shatters into fragments, and those fragments are deliberately put back to two sided, because a building shell genuinely is hollow once you break it open and you genuinely can see the inside.
The navy shards, explained
Which brings the post back to what Adam saw. The shatter fragments are two sided on purpose, and each one carries an outline shell sharing its exact shape and position. Wherever the nearest surface facing the camera happened to be an inside surface, the outline shell won the tie and flooded the whole fragment with outline colour.
It was intermittent because whether a given fragment shows you its inside depends on which way the camera is pointing and where that fragment randomly came to rest, which is why it presented as an occasional glitch rather than something reproducible. Nothing crashed, nothing logged, and no test could have caught it. It was found by watching buildings die. The outline shells now sit fractionally in front, and the fragments light correctly.
That bug existed only because inside surfaces were being drawn at all. A two-sided default across an entire asset library does not merely waste shading work, it changes which surface wins a tie for the nearest thing to the camera.
Auditing your own exporter defaults
The general form has nothing to do with graphics. A default that arrives with an asset was never chosen for your project, and nothing downstream will tell you it is there. The way to tell a default from a decision is to count the field across everything you have rather than to look at the one model in front of you. A field with a spread of values was chosen case by case. A field with one value across twenty thousand files was written by a tool, and which tool matters far less than you would think, because you are free to overrule it either way.
Three fields are the ones to count in any glTF pipeline, and the same JSON-only reader answers all three with one line changed: whether materials are two sided, whether they are transparent, and whether a glow-strength extension is present. Each is written silently by an exporter, each costs something at runtime, and none of them will ever show up in your project as a change, because the value was fixed before the file arrived.
This shipped alongside collapsing 1,392 unit draw requests into 96, and it rode along because the audit was cheap once the scanner existed, and a 100% hit rate across a whole asset library does not come around twice.





