Second Chance GazetteDevlogToken Use

Collapsed to a Point

unreal-engineblenderstatic-meshmaterial-instancemetahumancollisionsilent-failure

Six in the morning, and the takeaway fan

The session had been left running overnight, building twelve cars.

They are fictional to the badge, generated by a Python script in Blender rather than bought in, and they are modelled off 323 photographs of real second-hand adverts, which is how you end up with right-hand drive, a proper cowl, and doors and boots that open. The whole set builds and exports headless in about thirty-one seconds. James went to bed with his eyes gone from reference images and left it to it.

What woke him at six was the extractor fan on the takeaway outside, not the cars. He checked them anyway. They had come out well, well enough that he spent breakfast giving notes rather than starting again, and the notes came fast: the dashboard too big, the front seats sitting too far back, the rear headrests poking up through the back windscreen, the parcel shelf standing still while the tailgate lifted over it. He wanted the shelf on its strings, the way it is in a real hatchback. He also wanted twelve different interiors rather than one interior twelve times, which is fair, because a fleet of identical cabins reads as wallpaper the moment you look through two windscreens at once.

A row of untextured grey and silver cars parked on grass with their front doors, rear doors and tailgates all standing open at once. The nearest three are hatchbacks with the boot floor and inner tailgate liner clearly visible; red rear lamp panels and black bumper mouldings break up the pale bodywork. Red-brick terraced houses with chimney stacks and a larger brick building run along the skyline behind.
Twelve cars in a row with every door and tailgate posed open for the design review. Three hours later the boots were a different shape and the seats had moved forward.

The agent, Claude Code Opus 5, working through two editor servers, took all of it and rebuilt the packaging around a standard set of measurements instead of a slab across the cabin: wheel centre behind the cowl, hip point derived from knee room, backrest hinge landing on the B-pillar. Then a table of per-car cabins, read off the advert galleries. Fabric or leather. Hooded binnacle or flat fascia or a tablet stuck on top. Manual, automatic or a rotary selector. Rear bench or two buckets. It was the right answer and it was answering the wrong question, but that would not surface for another two hours.

The handle that was not connected to anything

One of the notes was that the interior door handle was not attached to the door.

That reads like a placement problem, and it was treated as one. The armrest and the pull were made to measure the door's local depth at their own height, because the tumblehome, the way a car's sides lean inward towards the roof, had been leaving them floating an inch clear of the trim. That fix was real and it was needed.

It was not the fault. The door card, the trim panel on the inside of the door, did not exist. None of them did.

The driver's-eye view from inside one of the cars: a two-tier dashboard in pale tan plastic with a dark instrument binnacle, a centre stack of vents and rotary controls, a steering wheel rim across the bottom of the frame, and a rear-view mirror hanging from the top of the windscreen. Through the glass, the other cars stand on grass with their doors open and brick buildings behind.
The driver's-eye view. The dashboard is a shallow shelf over a fascia band with open knee room, and the interior plastic still reads pale tan under full sun, which nobody has explained yet.

Blender's mesh library gives a newly created vertex an index of minus one until something tells it otherwise. There is an obvious-looking call for this, and it is the wrong one: ensuring the lookup table does not assign indices. Updating the index does. The copy routine that builds every inner liner in the car read the index straight off the new vertices, got minus one for all of them, and welded each liner onto a single point. Every door card, every tailgate liner, the whole boot tub. Twenty-seven thousand of the interior's thirty-seven thousand triangles had no area at all.

It survived because nothing complained. The build ran clean. The renders looked fine, because what is not there does not draw. Nine other liners did come out correct, and that was the confusing part until it was not: those parts happen to have a mesh operation run over them on the way through, and an operation assigns indices as a side effect. The ones that went straight from creation to export were the ones that vanished.

What caught it was a guard nobody had put there for this. Unreal drops zero-area triangles on import, so the imported bounds came back four to eight per cent smaller than the figures the exporter had written down for the same mesh, and the import script compares those two numbers as a matter of course. A car being four per cent smaller than its own manifest is not a plausible rounding error. That is the whole detection story. Rockstar have somebody whose job is to open the asset and look at it. Here the asset is checked by a script written for a different reason on a different day.

What Rockstar's people are not allowed to say

After breakfast he read X, and X was full of GTA 6. A large quantity of the game's assets had leaked, and Rockstar had posted a reminder to the people who built it that they are not to mention their involvement until 19 November. Then a video from somebody who had been to a preview at Rockstar HQ, going through the open questions. No casino has been mentioned. No job you can clock on to has appeared in anything released or leaked. Hunting for profit is confirmed. The fishing is catch and release.

That is the sort of thing that decides a Sunday.

A roulette wheel set into a green baize table, seen from above and slightly to one side. The polished wood bowl holds a numbered ring of red, black and green pockets with a gold turret and spindle at the centre. A rack of stacked coloured chips and a printed betting layout sit at the top right of the frame.
A roulette wheel in the bookmaker's on the corner. Second Chance has a casino because nobody had to be persuaded to build one.

The jobs line is the one worth arguing, because it is the one Second Chance already answers. There is a refuse and fly-tipping shift you clock on to and get paid for, and a job at the pub. There is a council bin lorry working a terraced street with two loaders on the kerbs, their lift technique corrected by a man who does it for a living. The e-bike for the takeaway delivery gig is finished, down to the charge lights on the battery, and the gig itself is being built around it. A world where you are out on a release programme and can go and do a day's work in it is a different proposition from a world where the work is a cutscene about somebody else's life.

Two British fruit machines standing side by side against a cream wall in a betting shop, lit by daylight from a window between them. The left machine's glass reads River of Riches in gold on blue, the right one Country Escapes in gold on green, each with a three-in-a-line prize table, three reels, a nudge ladder, a row of coloured feature lamps and a red button deck. A blue scratchcard display board stands at the left edge.
Two fruit machines in the betting shop, River of Riches and Country Escapes, both playable.

And the NDA is the other half of it, which is James's own observation and the best line he has had in a fortnight: the people who built the biggest game in the medium are under orders to say nothing about it for two months. This one publishes every week, screenshots and faults included, and would be delighted if more people talked about it. Being small is why the whole build can go out in public. Nobody has to approve it.

A colour that had never been set

The gambler at the roulette table had been wearing clothes made by hand in Blender. They came off, and customised Fab garments went on. The other man got customised MetaHuman clothing, and looks considerably better for it.

Then the trainers, which took the rest of the evening. James set their colour in the MetaHuman editor, came back to them later, and reported that it had reverted.

It had not reverted. It had never arrived.

A close crop of a man's legs and feet on the patterned carpet of a betting shop. He is wearing dark grey trousers and a grey trainer with a bright saturated blue sole and blue laces. A dark table leg and a blue skirting stripe run through the frame.
Grey upper, bright blue sole. Every colour in this frame is inherited from the parent material, and a script had already confirmed all of them.

A material instance in Unreal can inherit every value from its parent and hold none of its own. This one held none of its own: zero overrides on the whole instance. A script had read the colours off it, saved them, read them back identical and reported success, and all of that was true and none of it meant anything, because the readback returns the inherited value and cannot distinguish "set to this" from "never set at all". The character edit had gone onto the character at ten to five. The outfit assets it should have reached had been built at twenty past four, half an hour earlier, and nothing rewrote them.

The sole was a separate fault and a better one. Black went on the upper and the sole stayed white, and two confident explanations came and went: one parameter turned out to control the laces, and flipping another moved the sole by 3.7 sRGB, which is nothing. The actual cause was a painted wear overlay shipping at 0.95 strength, washing the sole pale regardless of what colour it was told to be. Taking it to zero dropped the sole band by 21.6 sRGB with the upper unmoved.

The same man's legs and feet on the same betting shop carpet after the fix: the trainer is now black with a plain dark grey sole and grey laces, and the blue has gone entirely. A red engine warning about video memory runs across the top left of the editor viewport.
Black upper, grey sole, painted wear off. The difference between this and the failed attempt is about eighteen points of brightness, which is invisible by eye and obvious on a crop.

The method is the reusable part, and it is duller than looking: crop the same box out of the before and after frames and take the mean. A 3.7 difference and a 21.6 difference look identical on a screen.

The capture script that produced those frames had raised the editor to Epic quality and never put it back, which is why the room was reporting itself 1.5 to 2.2 GB over the video memory budget on an 8 GB card for the rest of the session. Pair every override with its restore, in the same script.

An eye eleven millimetres inside a head

The gym was the middle of the day. A mile running, then half an hour in the pool, which was warm. Outside it was warm and windy, and they were putting the fair up on the recreation ground next door, so there were people stood watching the trucks come in with the rollercoaster.

The animals got the afternoon, for the same reason as the trainers: they are seen close up now. A long-lens rig was built so each species can be photographed properly instead of squinted at across a field, and the first otter through it had no eye. Just fur where the eye should be.

Every mammal eye in the set was seated inside its own head. The otter's was eleven millimetres in, the vole's eight, the fox's flush with the skin. Eye centres are now pushed out to sit at 45 per cent of their radius inside the surface, and the builder prints where it put each one. While it was open: the legs had been built with seven sides, and a seven-sided tube has 51 degree corners, which is over the 46 degree threshold at which the renderer stops smoothing. Ten sides, and they are legs again.

A long-lensed shot of a brown otter walking through tall grass in low sun, seen from the side against a blown-out pale background. Its coat reads as short wet fur, the black nose and whiskers catch the light, and a single dark eye is clearly visible on the side of the head.
The otter after both fixes, in long grass. The first version of this shot had it standing on the river bed, because the chosen spot was the slope down into the water rather than the bank.

A light that spawned with its collision on

The last of it was the delivery job, and it produced the cleanest lie of the day.

A small movable light needed to be spawned into the world while the game is running, and it needed no collision. Setting it to none persisted. The write echoed the new value back. Read the template afterwards and it still said none. Spawn the thing and it comes up with collision fully enabled.

Unreal stores that setting inside a structure that also holds the collision profile by name, and writing to the structure merges rather than replaces, so the old profile survived the edit untouched. On spawn the profile reapplies its own setting over the raw one. The template genuinely did hold the right value. It was overwritten a moment after the actor existed.

An e-bike parked on a tarmac road at night, seen from above and behind. A large red insulated delivery box lettered YOU CHEW in yellow sits on the rear rack, the frame carries a battery pack with green charge lights, and a phone bracket is clamped to the handlebars. A white road line, a drain cover and a low brick garden wall run behind it.
The e-bike, finished. The job that will run on it is still being built.

The fix is to set the profile by name in the same structure, which is the only way to reach it, because the profile field cannot be written on its own. The wider point is the one that cost the round: for anything created while the game is running, a template readback is not evidence. Spawn it, read the live thing, destroy it.

That was Sunday. The cars are in the engine and the boots open. The snooker went up on YouTube.

The fault

A mesh imports into Unreal Engine 5.8 with smaller bounds than the file it came from

Nothing errors. The exporter reports a clean build, the renders look correct, and the imported static mesh reads four to eight per cent under the dimensions recorded for it at export.

Cause: in Blender's mesh library a vertex created with verts.new() carries index -1 until something assigns one. ensure_lookup_table() does not assign indices; index_update() does. A copy routine reading v.index therefore welds every new vertex onto element zero, and the whole part collapses to a single point.

The collapsed parts do not disappear from the file. They export as zero-area triangles, which Unreal drops on import, which is where the missing volume goes. In this build 27,000 of an interior's 37,000 triangles were zero-area, every inner liner was gone, and the visible symptom was "the interior door handle is not connected to the door", because the panel the handle mounts to did not exist.

☠ Parts that survived did so by accident: any bmesh operation run over a part assigns indices as a side effect, so a routine that calls recalc_face_normals or an extrude happens to be safe and one that goes straight from creation to export is not.

The fix

Call index_update() before reading any vertex index, and compare imported bounds against the exporter's manifest

Two changes, and the second is the one that will catch the next unrelated version of this:

  • The copy routine calls index_update() before it reads v.index, and the triangulated

export copy drops zero-area triangles rather than shipping them.

  • The exporter writes each mesh's dimensions into a manifest, and the import script asserts

the imported bounds against it. A four per cent discrepancy is not a tolerance, it is missing geometry.

The guard: a geometry fault that produces zero-area triangles is invisible to every check that counts objects, materials or file sizes. Dimensions are the cheapest thing that notices.

The fault

A material instance readback returns the value you asked for and the instance overrides nothing

A colour is set on a character, saved, read back identical, and reported verified. On screen the old colour is still there, and the report will come back as "it reverted".

Cause: get_material_instance_vector_parameter_value() returns the inherited value from the parent material when the instance holds no override of its own. It cannot distinguish "set to this" from "never set". A save-and-readback of an inherited value passes perfectly with nothing behind it.

Second fault in the same afternoon, same character: a region that ignores every colour parameter you can reach. M_shs_casualsneakers ships WearMaskStrength at 0.95 with painted wear enabled, which washes the sole pale whatever tint it is given.

The fix

Count the instance's own overrides, and measure the pixels instead of looking

Read what the instance asserts, not what it answers:

mi.get_editor_property("vector_parameter_values")   # count this

A count of 0 means every "verified" value on that instance is the parent's. The colour script now asserts the count before and after and faults if a parameter is not present as a real override. Overrides went 0 to 3, and the colour arrived.

For the wear overlay: WearMaskStrength 0.95 to 0.0 moved the sole band 21.6 sRGB with the upper unchanged, against 3.7 for the hypothesis that felt right. Crop the identical box out of the before and after frames and take the mean. By eye those two results look the same.

The guard: if a character's built outfit assets predate a wardrobe edit, that edit is not in them. Compare the folder timestamps before trusting anything downstream.

The fault

A component set to NoCollision spawns at runtime with collision enabled

set_component_property BodyInstance = "(CollisionEnabled=NoCollision)" returns persisted: true, echoes the new value, and a read of the template afterwards agrees. The spawned actor comes up QUERY_AND_PHYSICS.

Cause: BodyInstance also carries CollisionProfileName, and a struct write is a merge, so the untouched profile (BlockAllDynamic) survives inside the same struct. On spawn the profile reapplies its own collision setting over the raw enum. The template really did hold NoCollision; it was overwritten after the actor existed.

The fix

Set the profile by name inside the same struct, and verify by spawning

Write the whole thing at once:

(CollisionProfileName="NoCollision",CollisionEnabled=NoCollision,ObjectType=ECC_WorldDynamic)

CollisionProfileName is not settable on its own. set_component_property answers "Property 'CollisionProfileName' not found on StaticMeshComponent", because it lives inside BodyInstance.

The guard: a component template readback is not a spawn readback. For anything created while the game is running, spawn it, read the live component, destroy it, and wrap that probe in try/finally or a raised exception leaves the actor in the map.

The kit

ToolWhy this one
Blender, driven headlesstwelve cars build and export in about 31 seconds with no editor open and nothing to wedge. The interactive route had already run a UV unwrap for four hours and locked up.
Unreal Engine 5.8where the collapsed liners, the inherited colours and the collision profile were all visible, and none of them visible anywhere else
An exporter manifest of per-mesh dimensionsthe only thing that noticed the geometry was missing
A crop-and-mean pixel comparisonsettled a colour question that two confident readings had got wrong
323 photographs of real second-hand car advertsthe source for every profile number, cabin layout and trim choice in the twelve

Plain English

TermWhat it means
static meshA 3D model that does not bend - a wall, a bin, a bottle. It can be moved as a whole, but its shape is fixed.
material instanceA copy of a material with some settings changed. Cheap to make and cheap to render, so most surfaces are instances rather than originals.
boundsThe invisible box the engine considers an object to occupy. Streaming and culling decisions are made on the box, not on the model, so an object with a box the size of the map can never be streamed out.
tri countHow many triangles a model is made of. The basic measure of how expensive it is to draw.
pivotThe point a model rotates and scales around, and the point it sits on when placed. A pivot in the wrong place makes an object float or sink.
FBXThe interchange file format used to move models and animation between Blender and Unreal.
actorAnything placed in the game world - a lamp post, a person, a trigger volume, a camera. If it sits in the level, it is an actor.
MetaHumanEpic's system for realistic human characters - face, body, hair and clothing, ready to animate. Every named person in the game is one.
VRAMMemory on the graphics card. This project has 8 GB of it, and that ceiling shapes most of the decisions in the devlog.
scalability settingsThe engine's quality dials - shadows, textures, effects, anti-aliasing - each of which can be set independently. Turning one up can cost far more than turning the rest up together.
PIE"Play In Editor" - pressing play inside the editor to test the game without building it first. Fast, and it behaves subtly differently from the real build.
MCP serverA bridge that lets an AI agent drive another program directly. Here, two of them let the agent operate the Unreal editor.

Things we still need to fix tomorrow:

  • The parcel shelf lifting with the tailgate is built and the next job is pressing it in play

and watching it swing.

  • The twelve cars are about 167,000 triangles each at full detail against a target of 80,000

to 120,000. The boot tub liner is most of it.

  • The interior plastic reads pale tan under full sun. Check the material's own base colour

before blaming the lighting.

  • The badger's ear is a flat black coin, the rabbit's hind legs are tubes that want haunches,

and the pigeon's wing bars still read as stickers. Nine more species to photograph.

  • Loading the main map twice in one editor session took the editor down with a D3D12 fatal

error twice yesterday. Order a session as assets first, then one load of the world.

  • The refuse induction film and the snooker are up. The delivery gig is the next thing being

built.

← All devlog entries

Watch it get built. All of this goes up on YouTube as it happens - broken animations, buildings hovering a foot off the ground, the lot.

Subscribe on YouTube