---
title: "Model authoring parametric primitives and CSG for custom meshes"
description: "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."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/knowledge/playbooks/model-authoring-parametric-csg/
---

# Model authoring parametric primitives and CSG for custom meshes

## When to use

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.

## 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.
