Surface Projection
The injected SurfaceProjection strategy that powers each topology's GPU position/normal assembly and CPU query/raycast/LOD — and how to add a new surface shape
A surface projection is the strategy a topology injects to describe how its surface is shaped and queried. It is the single extension point for adding new surface shapes: every built-in topology carries a SurfaceProjection, and the rest of the pipeline — the GPU vertex/normal stages, the CPU terrain cache, the query objects, the raycaster, and the LOD camera offset — calls into it rather than branching on a projection kind.
This is dependency injection rather than a switch/if (projection === ...) ladder: adding the torus required zero changes to the GPU pipeline, the CPU query/raycast machinery, or the React runtime.
The interface
interface SurfaceProjection {
/** Identifier for debugging/telemetry only — never switched on internally. */
readonly kind: "flat" | "cubeSphere" | "torus" | (string & {});
/** Representative radius (bounds/uniform helper); undefined for flat. */
readonly radius?: number;
/** Surface center in world space; undefined for flat. */
readonly center?: { x: number; y: number; z: number };
/** Closed surfaces face outward → the mesh flips its triangle winding. */
readonly faceOutward: boolean;
/**
* Per-axis level-0 tile count before LOD subdivision. Defaults to `{ u: 1, v: 1 }`.
* At level `L`, resolution is `baseU × 2^L` by `baseV × 2^L` tiles.
*/
readonly baseResolution?: { u: number; v: number };
gpu: SurfaceProjectionGpu; // TSL builders for the render + compute stages
cpu: SurfaceProjectionCpu; // query / raycast / LOD on the CPU snapshot
}GPU hooks (gpu)
| Hook | Purpose |
|---|---|
renderVertexPosition(ctx) | Render-stage world position for a vertex; also assigns the vertex normal varying. |
createTileComputeParts(ctx) | Projection-specific compute-stage builders: tile size, root UV, and the tile vertex world position. |
createFieldNormal(ctx) | The field-stage surface-normal reconstruction. |
augmentSampler?(sampler, params) | Optional extra GPU samplers (e.g. the cube-sphere's ByDirection samplers). |
CPU hooks (cpu)
| Hook | Purpose |
|---|---|
createSurfaceOps() | The surface math injected into the terrain cache (position → (space, u, v), displaced position, normal); null for flat. |
createRuntimeQueries(cache) | Build the runtime query objects: the flat query, the generic surfaceQuery, and (cube-sphere) the sphereQuery. |
raycast(ctx) | The projection-specific CPU ray pick against the displaced surface; returns null when the ray misses or the query snapshot isn't ready. |
Built-in projections
| Factory | Kind | Notes |
|---|---|---|
createFlatProjection() | flat | XZ heightfield; no surface query (surfaceQuery is null). |
createCubeSphereProjection({ radius, center, invert }) | cubeSphere | Radial sphere mapping; exposes surfaceQuery and sphereQuery. |
createTorusProjection({ majorRadius, minorRadius, center, invert, baseU, baseV }) | torus | Closed donut; exposes surfaceQuery. baseU/baseV default to 1; the topology auto-sets baseU = round(major/minor). |
You normally never construct these directly — the topology factories (createFlatTopology, createCubeSphereTopology, createTorusTopology, …) wire the matching projection for you.
Adding a new surface shape
To add a new closed surface you implement one SurfaceProjection and a small Topology:
- Map math — write the forward map
(u, v) → world position(GPU + CPU) and the inverseworld position → (u, v), plus an outward normal. gpuhooks — assemble the displaced vertex position and reconstruct the normal from displaced neighbor positions (curvature-correct, frame-independent).cpuhooks — providecreateSurfaceOps()(used by the terrain cache),createRuntimeQueries()(wraps the cache asquery/surfaceQuery), andraycast().- Topology — emit root tiles, resolve same-level neighbors (wrap for periodic axes), and write conservative
tileBounds. Setprojectionto your new strategy.
The torus source is the reference implementation; it reuses the shared curved-surface render/normal helpers, so the projection itself is small.
Related pages
- Topology Types — the topologies that inject these projections
- Terrain Query — the runtime
query/surfaceQuery/sphereQueryobjects - Torus example — a new surface added purely via projection injection