What sits between a generated mesh and a WebXR scene

The generation step got fast. The steps on either side of it did not, and the gap between them is where the time actually goes.

10 min read

The demo and the deployment are different problems

A text-to-3D model will hand you a textured mesh from a sentence. Viewed in the tool that produced it, spinning on a turntable against a neutral background, it looks finished. Drop the same file into a WebXR scene running on a standalone headset and the result is usually some combination of: enormous, wrongly oriented, sunk through the floor, shading incorrectly, and costing more frame time than everything else in the scene combined.

None of that means the generation was bad. It means the output is a viewer asset and the requirement is a runtime asset, and the distance between those two has not shortened just because the first step got quick. A useful way to hold it: generation replaced the modelling, and modelling was never the expensive part of shipping a 3D object into an interactive application.

The model does not know how big a metre is

This is the first wall, it costs about twenty minutes, and it catches everyone once. WebXR is a metric system: a reference space anchors to the floor, a hand is roughly where a hand is, and a chair that is 0.9 units tall is 0.9 metres tall. A generated mesh has no unit. It has a bounding box in whatever arbitrary scale the model settled on, and the relationship between that and a metre is undefined.

So you import a chair and it is eleven metres tall, or four centimetres. You scale it by eye, which works until the next asset arrives at a different arbitrary scale and now nothing in the scene is consistent with anything else. The fix is to normalise on import rather than per asset: pick a real-world dimension you know, compute the scale factor from the bounding box, and apply it. It is ten lines of code and it is the single highest-value thing you can automate in this pipeline.

The origin has the same problem in a quieter form. A generated mesh is typically centred on its bounding box, which means placing it at a hit-test point buries half of it in the floor. Objects that sit on surfaces want their origin at the base; objects that hang want it at the attachment point. No generator knows which yours is, because that is a fact about your application and not about the shape.

Normalising scale and origin on import

Run this at load time on every asset, generated or not. Making it unconditional is the point: a pipeline where some assets are normalised and some are trusted is a pipeline where you will spend an afternoon on the one that was trusted and should not have been.

import { Box3, Vector3 } from 'three';

// targetHeight is in metres, and it is a decision about your scene,
// not a property of the file. A dining chair is about 0.9.
function normalise(object, targetHeight, anchor = 'base') {
  const box = new Box3().setFromObject(object);
  const size = box.getSize(new Vector3());

  const scale = targetHeight / size.y;
  object.scale.multiplyScalar(scale);

  // Recompute after scaling: the old box is in the old units.
  const scaled = new Box3().setFromObject(object);
  const centre = scaled.getCenter(new Vector3());

  object.position.x -= centre.x;
  object.position.z -= centre.z;
  object.position.y -= anchor === 'base' ? scaled.min.y : centre.y;

  return object;
}

Recomputing the bounding box after scaling matters more than it looks. Scaling an object does not update a Box3 you captured earlier, and subtracting a pre-scale centre from a post-scale object is an offset error that scales with the asset — which makes it look like a different bug on every model.

Triangle count is the honest number, and it is usually wrong

Generated meshes tend to arrive dense. The reconstruction step has no reason to be frugal, and a few hundred thousand triangles for a single prop is ordinary. On a desktop that renders fine. On a standalone headset drawing two views at 90Hz, one such prop can consume a visible share of the frame.

Decimation tools will bring the count down, and for background objects that is often the whole answer. For anything the user gets close to, automatic decimation degrades in a specific way: it protects the silhouette and destroys the small features, so the object stays recognisable from across the room and turns to mush in the hand. Which is exactly backwards from what an XR scene needs, because in XR the user can always walk up to something.

The real cost is rarely one hero object anyway. It is thirty generated props, each individually acceptable, each with its own material, none of them instanced or batched, producing a draw call count that no amount of triangle reduction will fix. Watching the triangle number while the draw call number climbs is a very common way to optimise for an hour and change nothing.

What the generator hands you, and what a runtime asset needs

The items are ordered by how often they turn out to be the thing that actually blocked shipping.

Topology
Generated meshes are usually reconstructed surfaces rather than modelled ones: dense, irregular, and with no edge loops where a deformation would need them. Fine for a static prop. Unusable for anything that has to bend, and retopology remains manual work.
UV layout
Where UVs exist at all they are often automatic and wasteful, which shows up as texture memory rather than as a visible defect. It becomes visible the moment you want to bake lighting or place a decal, because both need a layout someone thought about.
Material channels
Most output is a baked colour texture, sometimes with lighting already in it. A WebXR scene with real-time lights wants roughness and metalness separated out; a diffuse map with highlights painted in will look wrong from every angle except the one it was generated from.
Collision geometry
There is none. Using the render mesh for physics on a dense generated object is the expensive way to do it, and generating a convex proxy is a step nobody does by hand unless the pipeline forgets to.
LODs
There are none of these either. A scene that generates twenty props and draws all of them at full detail regardless of distance is spending most of its budget on pixels a few metres away.
Scale and origin
Covered above, and listed again here because it is the one that gets fixed by hand per asset when it should have been fixed once at import.

Where the time genuinely went down

It would be wrong to read the list above as a verdict. Two things did get dramatically cheaper, and both of them matter.

Iterating on what a thing should look like used to require somebody to model it first. Now you can look at nine variants of the prop before committing, which changes what gets considered rather than just how fast it gets made. For anything where the design is not yet settled — and in XR the design is usually not settled, because how something reads at arm's length is hard to predict — this is a real change in how the work goes.

Texture generation landed more cleanly than geometry generation, and for an understandable reason: a texture is an image, image models are mature, and a slightly odd texture is a slightly odd texture rather than a broken asset. Retexturing an existing well-built mesh is one of the few places in this pipeline where the automated result goes straight into the scene.

The pattern across both is that generation is strongest where the output is evaluated by eye and weakest where the output has to satisfy a constraint the model was never shown. Frame budget, collision correctness, and a metre being a metre are all constraints of the second kind.

A pipeline that assumes the mesh is untrusted

The practical arrangement is to treat generated assets as raw input to a conditioning step rather than as finished files, and to run that step on everything so there is no trusted path to forget about.

That step does the mechanical parts: normalise scale against a known dimension, move the origin to base or attachment point, generate a convex collision proxy, produce two or three LODs, and check the triangle and material count against a budget that fails loudly. None of this is novel; it is the same conditioning any asset pipeline has always done. What changed is the volume, because generation makes it trivial to produce forty props for a scene that previously would have had six, and a manual conditioning step that was tolerable at six is the bottleneck at forty.

The parts that remain manual are the parts that were always the expensive ones: topology for anything that deforms, a UV layout for anything that gets baked or decalled, and the judgement about which objects are worth the budget. Text-to-3D moved the cheap part of the work, and a pipeline built on the assumption that it moved the whole thing will spend its savings on rework.

Related pages