Recipes
Model authoring parametric primitives and CSG for custom meshes
The catalog has no fitting mesh and you must build a shape: a creature, a prop, a weapon, a piece of furniture. This is constructive geometry. You assemble parametric primitives and combine them with boolean operations. It is the real path to a custom creature; you cannot hand-place vertices, but a surprising range of shapes falls out of primitives plus CSG.
Use this for
authoring custom meshes and creatures from scratch; combining primitives into one shape; carving holes and sockets with boolean subtract; building props furniture and low-poly characters; the real path to a Wrapplings-style custom creature
Not for
raw vertex or index buffers (not authorable, see capability-ceiling); marching-cubes or per-frame deformed geometry (engine gap); voxel blocks (see terrain-and-heightmaps)
Pairs with: Materials and looks PBR surfaces color metalness and textures, Asset acquisition strategy, Capability ceiling what the agent cannot author and the in-envelope fallback
The recipe
- Create a model asset:
project_create_asset(project.asset.create) withassetKind: "model". (You may seed the document inline viarendering.model, or save it in the next step.) - Author a
ModelAuthoringDocument. Its shape:metadata(name,createdAt,updatedAtrequired),materials(must include slotindex: 0), andsubMeshes(a discriminated array onprimitiveType). - Add primitive sub-meshes.
primitiveTypeis one ofbox,sphere,cylinder,capsule,cone,torus,plane,imported,csg_result. Each carriesid,name,parameters(per type, all defaulted), atransform(position/rotation/scale, radians for rotation), andmaterialSlot(defaults 0). Position the parts to build the silhouette. - Combine or carve with CSG: add a
csg_resultsub-mesh whosecsgOperationshas at least one entry{ id, type, targetSubMeshId, toolSubMeshId, keepOriginals, appliedAt }.typeisunion(weld two parts),subtract(carve the tool out of the target), orintersect(keep the overlap). SetkeepOriginals: falseto drop the source parts from the baked output. - Save:
model_save(project.asset.model_document.save) with{ ifVersion: <asset current version>, document: <the document>, source: "ai" }. The write is version-guarded; pass the asset's current version. - Attach to an entity:
RenderableComponent{ "type":"RenderableComponent", "shape":"mesh", "assetId": <model asset id>, "visible": true }withproject_add_component. SetmaterialRefto a material asset id to skin it (see materials-and-looks).shapemust bemesh.
Worked example: a low-poly creature
A four-legged critter: a capsule body, a sphere head with a mouth carved by subtract, a cone snout, two cylinder legs. The head is a csg_result that subtracts a hidden box cutter from a base sphere.
{
"metadata": { "name": "Wrappling", "createdAt": "2026-08-04T00:00:00Z", "updatedAt": "2026-08-04T00:00:00Z" },
"materials": [{ "index": 0, "name": "Body" }],
"subMeshes": [
{ "id": "body", "name": "Body", "primitiveType": "capsule",
"parameters": { "radius": 0.5, "height": 0.8 },
"transform": { "position": { "x": 0, "y": 0.7, "z": 0 } } },
{ "id": "head_base", "name": "Head base", "primitiveType": "sphere",
"parameters": { "radius": 0.4 },
"transform": { "position": { "x": 0, "y": 1.35, "z": 0 } } },
{ "id": "mouth_cutter", "name": "Mouth cutter", "primitiveType": "box",
"parameters": { "width": 0.5, "height": 0.12, "depth": 0.3 }, "visible": false,
"transform": { "position": { "x": 0, "y": 1.28, "z": 0.3 } } },
{ "id": "head", "name": "Head", "primitiveType": "csg_result",
"parameters": { "resultKind": "baked" },
"csgOperations": [
{ "id": "carve_mouth", "type": "subtract", "targetSubMeshId": "head_base",
"toolSubMeshId": "mouth_cutter", "keepOriginals": false,
"appliedAt": "2026-08-04T00:00:00Z" } ] },
{ "id": "snout", "name": "Snout", "primitiveType": "cone",
"parameters": { "radius": 0.16, "height": 0.35 },
"transform": { "position": { "x": 0, "y": 1.3, "z": 0.42 }, "rotation": { "x": 1.5708, "y": 0, "z": 0 } } },
{ "id": "leg_l", "name": "Leg L", "primitiveType": "cylinder",
"parameters": { "radiusTop": 0.12, "radiusBottom": 0.12, "height": 0.5 },
"transform": { "position": { "x": -0.28, "y": 0.25, "z": 0.12 } } },
{ "id": "leg_r", "name": "Leg R", "primitiveType": "cylinder",
"parameters": { "radiusTop": 0.12, "radiusBottom": 0.12, "height": 0.5 },
"transform": { "position": { "x": 0.28, "y": 0.25, "z": 0.12 } } }
]
}
Pitfalls
materialsMUST contain slotindex: 0; every sub-meshmaterialSlotmust reference a declared slot. Omitting materials uses the default single slot.- Sub-mesh
idvalues must be unique and match^[a-z0-9][a-z0-9._:-]*$. CSGtargetSubMeshId/toolSubMeshIdmust name real sub-meshes. csg_resultrequires at least onecsgOperationsentry;appliedAtmust be an ISO datetime.- Rotation is radians, not degrees (half pi is about 1.5708).
RenderableComponent.shapemust bemeshfor a model asset; leaving itsphereignores your mesh.- No raw vertex buffers exist. If the shape cannot be built from primitives plus CSG, generate or import a gltf/glb/obj asset instead (
primitiveTypeimported), or fall back to the catalog.
Verify
project_get_graph_snapshotto confirm the model asset and the entity'sRenderableComponent(shape: "mesh", correctassetId).qa_capture_renderer_viewportto confirm the assembled mesh renders.