Skip to content

Mesh beta

Scriptable dynamic mesh with CPU-driven vertex and index updates.

Reached via entity.representation.mesh on representations created with createMesh(...). Returns null for any other representation kind.

Write methods (writeVertices, writeIndices) are designed to run every frame: each call issues a single memcpy from the JS TypedArray's backing buffer directly into the mesh's GPU-backed storage, with no intermediate copies. Combine with setBounds / drawCount (single-part shorthand) or updatePart (multi-part) to publish the new draw range each tick.

Properties

attributes

  • Type: any
  • Read-only summary of the mesh's attribute layout: an object keyed by attribute name with { semantic, format, storage } fields. Useful for introspection ("does this mesh have a 'speed' attribute?") and for cross-engine devs who reach for geometry.attributes reflexively.

drawCount

  • Type: number
  • Convenience getter/setter for single-part meshes.

indexCapacity

  • Type: number
  • Maximum index count the mesh was allocated for.

partCount

  • Type: number
  • Number of parts (draw ranges) on the mesh.

vertexCapacity

  • Type: number
  • Maximum vertex count the mesh was allocated for. Read-only – the underlying buffer is sized at creation. To grow, create a new mesh.

Methods

recomputeBounds()

javascript
recomputeBounds(partIndex: number): BoundingBox | null

Recompute and apply min/max bounds for a part from its written vertex positions.

Walks the position attribute, writes the computed BoundingBox back onto the part, and returns it so callers can inspect the result.

Expensive on large meshes – walks partVertexCount × 3 floats. Most authors should track running bounds incrementally in their own per-frame loop (cheap) and only call recomputeBounds() during prototyping or on a sparse cadence. Returns null if the mesh has no position attribute or partIndex is out of range.

Example:

javascript
mesh.writeVertices("position", 0, scratch);
mesh.recomputeBounds(0);  // updates part 0's bounds to fit the written verts

Parameters:

  • partIndex (number) - Zero-based index of the part to recompute.

Returns: BoundingBox | null

replaceParts()

javascript
replaceParts(parts: Array<{ indexCount: number, indexOffset?: number, topology?: string, materialIndex?: number, bounds: BoundingBox | { min: number[3], max: number[3] } }>): void

Replace the entire parts list. Use for dynamic-part-count meshes where each frame's GPU dispatch produces a variable triangle count (marching cubes, fluid surface, voxel terrain). Pair with an atomic counter in outputBuffers to size the part:

updatePart(...) mutates one part in-place; replaceParts(...) swaps the entire list. The latter is the right choice when the part count itself changes between frames.

Example:

javascript
const [emitted] = triCounter.readSync();
mesh.replaceParts([{
    indexCount: emitted * 3,
    topology: 'triangle',
    bounds: { min: [-1, -1, -1], max: [1, 1, 1] }
}]);

Parameters:

  • parts (Array<{ indexCount: number, indexOffset?: number, topology?: string, materialIndex?: number, bounds: BoundingBox | { min: number[3], max: number[3] } }>)

Returns: void

runCompute()

javascript
runCompute(kernel: Kernel, options: { uniforms?: Float32Array, outputs?: string[], outputIndices?: boolean, inputBuffers?: object, outputBuffers?: object, inputTextures?: object, outputTextures?: object, threadGroups: number[3], threadsPerThreadgroup?: number[3] }): void

Dispatch a compute kernel against this mesh's storage: compute attribute buffers, optionally the index buffer, plus arbitrary input / output buffers and textures. The renderer waits on the dispatch's command buffer before sampling the mesh.

Check environment.features.has('compute') before calling – returns false on devices without GPU compute support.

MSL slot order is deterministic: buffer(0) uniforms → buffer(1..) outputs (mesh attributes) → outputIndices → inputBuffers → outputBuffers. Textures live in the separate texture(0..) slot space: inputTextures then outputTextures. Match your MSL signature to this order.

outputs lists attribute names whose buffers the kernel writes (must be declared storage: "compute"); outputIndices: true exposes the mesh's index buffer for GPU writes; inputBuffers / outputBuffers bind Buffer handles to kernel slots; textures bind via .read / .write tokens. threadsPerThreadgroup defaults to a SIMD-aligned shape per dispatch dimensionality (1D → [threadExecutionWidth, 1, 1]; 2D → [16, 16, 1]; 3D → [8, 8, 4]). Authors who know their kernel's register / shared-memory budget can override.

Example:

javascript
const kernel = await Kernel.fromSource({
    source: kernelSource,
    functionName: 'marchCubes'
});
const triCounter = Buffer.atomic(0);

scene.on('render', function () {
    mesh.runCompute(kernel, {
        uniforms: new Float32Array([scene.time, 0.016]),
        outputs: ['position', 'normal'],
        outputIndices: true,
        outputBuffers: { triCount: triCounter },
        threadGroups: [8, 8, 4]
    });

    const [emitted] = triCounter.readSync();
    mesh.drawCount = emitted * 3;
});

Parameters:

  • kernel (Kernel) - handle from Kernel.fromSource(...) / Kernel.fromAsset(...).
  • options ({ uniforms?: Float32Array, outputs?: string[], outputIndices?: boolean, inputBuffers?: object, outputBuffers?: object, inputTextures?: object, outputTextures?: object, threadGroups: number[3], threadsPerThreadgroup?: number[3] })

Returns: void

setBounds()

javascript
setBounds(bounds: BoundingBox | { min: number[3], max: number[3] }): void

Update the bounds of part 0.

Shorthand for updatePart(0, { bounds }) on single-part meshes. Accepts either a BoundingBox instance or a plain object { min: [x, y, z], max: [x, y, z] }.

Example:

javascript
mesh.setBounds(BoundingBox(Vector3(-1, 0, -1), Vector3(1, 2, 1)));
mesh.setBounds({ min: [-1, 0, -1], max: [1, 2, 1] });  // also valid

Parameters:

  • bounds (BoundingBox | { min: number[3], max: number[3] }) - New bounds for part 0.

Returns: void

updatePart()

javascript
updatePart(partIndex: number, options: { indexCount?: number, indexOffset?: number, topology?: string, materialIndex?: number, bounds?: BoundingBox | { min: number[3], max: number[3] } }): void

Update a part's metadata.

Only the keys present in options are applied; everything else is left untouched. The typical per-frame call for a growing mesh sends new bounds and/or indexCount for part 0.

For single-part meshes, the shorthands mesh.drawCount = N and mesh.setBounds(b) are equivalent to passing { indexCount: N } and { bounds: b } here.

Example:

javascript
mesh.updatePart(0, {
    indexCount: 1234,
    bounds: { min: [-1, 0, -1], max: [1, 2, 1] }
});

Parameters:

  • partIndex (number) - Zero-based index into parts.
  • options ({ indexCount?: number, indexOffset?: number, topology?: string, materialIndex?: number, bounds?: BoundingBox | { min: number[3], max: number[3] } }) - Only present keys are applied.

Returns: void

writeIndices()

javascript
writeIndices(indexOffset: number, data: Uint32Array | Uint16Array): void

Write indices starting at slot indexOffset.

The index count is derived from the TypedArray's length. data must be Uint32Array (the default index type) or Uint16Array if the mesh was created with indexType: "uint16".

Example:

javascript
mesh.writeIndices(0, new Uint32Array([0, 1, 2, 2, 1, 3]));

Parameters:

  • indexOffset (number) - First index slot to write into.
  • data (Uint32Array | Uint16Array) - Index data matching the mesh's indexType.

Returns: void

writeVertices()

javascript
writeVertices(name: string, vertexOffset: number, data: Float32Array | Uint8Array): void

Write packed elements of a single named attribute.

Starts at vertex index vertexOffset. The vertex count is derived from the TypedArray's length (array.length / componentsPerVertex).

data must be a JS TypedArray whose element type matches the attribute's format: Float32Array for float / float2 / float3 / float4, Uint8Array for uchar4Normalized. Bytes flow straight from the JS engine's buffer into the mesh via a single memcpy – no intermediate copy, no per-element trips.

Example:

javascript
mesh.writeVertices("position", 0, new Float32Array([
    -0.25, 0.0, 0.0,
     0.25, 0.0, 0.0,
     0.0,  0.4, 0.0
]));

Parameters:

  • name (string) - Attribute name as declared in createMesh.
  • vertexOffset (number) - First vertex slot to write into (0 for a full overwrite).
  • data (Float32Array | Uint8Array) - Packed attribute data. The TypedArray must match the attribute's declared format.

Returns: void