Gessa Docs
Recipes

Recipe

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.
engine v1.0.234since action-catalog.v1.0.232, model-authoring, rendering.v1Copy for LLM

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

  1. Create a model asset: project_create_asset (project.asset.create) with assetKind: "model". (You may seed the document inline via rendering.model, or save it in the next step.)
  2. Author a ModelAuthoringDocument. Its shape: metadata (name, createdAt, updatedAt required), materials (must include slot index: 0), and subMeshes (a discriminated array on primitiveType).
  3. Add primitive sub-meshes. primitiveType is one of box, sphere, cylinder, capsule, cone, torus, plane, imported, csg_result. Each carries id, name, parameters (per type, all defaulted), a transform (position/rotation/scale, radians for rotation), and materialSlot (defaults 0). Position the parts to build the silhouette.
  4. Combine or carve with CSG: add a csg_result sub-mesh whose csgOperations has at least one entry { id, type, targetSubMeshId, toolSubMeshId, keepOriginals, appliedAt }. type is union (weld two parts), subtract (carve the tool out of the target), or intersect (keep the overlap). Set keepOriginals: false to drop the source parts from the baked output.
  5. 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.
  6. Attach to an entity: RenderableComponent { "type":"RenderableComponent", "shape":"mesh", "assetId": <model asset id>, "visible": true } with project_add_component. Set materialRef to a material asset id to skin it (see materials-and-looks). shape must be mesh.

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.

JSON
{
  "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

  • materials MUST contain slot index: 0; every sub-mesh materialSlot must reference a declared slot. Omitting materials uses the default single slot.
  • Sub-mesh id values must be unique and match ^[a-z0-9][a-z0-9._:-]*$. CSG targetSubMeshId/toolSubMeshId must name real sub-meshes.
  • csg_result requires at least one csgOperations entry; appliedAt must be an ISO datetime.
  • Rotation is radians, not degrees (half pi is about 1.5708).
  • RenderableComponent.shape must be mesh for a model asset; leaving it sphere ignores 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 (primitiveType imported), or fall back to the catalog.

Verify

  • project_get_graph_snapshot to confirm the model asset and the entity's RenderableComponent (shape: "mesh", correct assetId).
  • qa_capture_renderer_viewport to confirm the assembled mesh renders.
Was this helpful?Report an issueContact support

On this page