Solid modelling, optimization and LOD

Model solids in code, then optimize them and prepare LOD levels.

Build a solid

A Solid is an immutable recipe: every operation returns a new Solid. Primitives are centred at the origin, and cylinders and tori stand on the Y axis.

createSolid(solid, options) tessellates, optimizes and registers the recipe once, then returns the same model for the same recipe. Call it after warmUpEngine, during setup; paint takes 0..1 tuples, and white parts can be tinted per entity.

Build a solid reference
Solid APIWhat it does
Solid.sphere(radius = 0.5) / ellipsoid(radii)Round primitives.
Solid.box(size, radius = 0)Full width, height and depth; rounding stays inside.
Solid.capsule(start, end, radius) / cylinder(radius, height)Rods and posts.
Solid.torus(radius, tube)Major radius to the tube centre, then the tube radius.
Solid.union(...solids) / a.union(b)Join parts; the static form balances many parts.
a.blend(b, width)Smooth weld over width world units.
a.subtract(b) / a.intersect(b)Drill holes, or keep only the overlap.
translate(v) / rotate(q) / scale(s | v)Move, turn or resize the recipe.
paint(color)Vertex color; paint after subtract to color the cut walls.
shell(thickness)Hollow wall of that full thickness.
displace({ amplitude, frequency?, seed?, octaves? })Seeded noise detail, 1..6 octaves (default 3).
toJSON()Serializable recipe for caching or inspection.

An arch with a doorway

React and React Native · shared engine API

An arch with a doorway
import { Solid, createSolid } from "@nayan-ui/engine";
// After warmUpEngine, once per app.
export const arch = createSolid(
Solid.box([4, 3, 1], 0.2)
.translate([0, 1.5, 0])
.subtract(Solid.capsule([0, 0, 0], [0, 1.5, 0], 0.8))
.paint([0.5, 0.48, 0.42]),
{ resolution: 48, center: false },
);

Tessellate, inspect and deform

solidGeometry(solid, { resolution, uv }) returns editable, unregistered Geometry: resolution is 8..128 cells across the longest side (default 48) and uv picks the planar projection 'xy' (default), 'xz' or 'yz'. Cost grows roughly cubically with resolution, so build during setup, never in onUpdate.

getGeometryStats reports vertices, triangles, bytes and bounds without registering anything. deformGeometry(geometry, (position, uv) => newPosition) sculpts a copy of one smooth surface and recomputes its normals.

A seeded rock

React and React Native · shared engine API

A seeded rock
import { Solid, createShape, deformGeometry, getGeometryStats, solidGeometry } from "@nayan-ui/engine";
export function createRock(seed: number) {
const recipe = Solid.ellipsoid([1, 0.6, 0.8]).displace({ amplitude: 0.08, frequency: 3, seed });
const raw = solidGeometry(recipe, { resolution: 32, uv: "xz" });
const flat = deformGeometry(raw, ([x, y, z]) => [x, Math.max(y, -0.3), z]); // flatten the base
console.log(getGeometryStats(flat)); // { vertices, triangles, bytes, bounds }
return createShape(flat, { optimize: true });
}

Optimize and build LOD levels

optimizeGeometry(geometry, { ratio, error, lockBorders }) returns a cleaned copy: ratio 1 (default) only welds and reorders, lower ratios simplify within error (default 0.001), and lockBorders (default true) keeps open edges fixed. Use it on static opaque meshes only, never on skinned meshes or geometry whose vertex indices other code reads.

createGeometryLods(geometry, ratios = [1, 0.5, 0.25], options) simplifies the original independently per level. There is no automatic LOD switch: register the levels once and swap an entity's mesh when its distance changes.

Three LOD meshes chosen by distance

React and React Native · shared engine API

Three LOD meshes chosen by distance
import { createGeometryLods, createShape, type Entity, type Geometry, type Model, type World } from "@nayan-ui/engine";
export function prepareLods(geometry: Geometry): Model[] {
return createGeometryLods(geometry, [1, 0.5, 0.25], { error: 0.01 }).map((level) => createShape(level));
}
export function chooseLod(world: World, entity: Entity, levels: Model[], distance: number) {
const mesh = levels[distance < 10 ? 0 : distance < 25 ? 1 : 2];
if (mesh) world.set(entity, { mesh });
}