Cubemaps¶
Render a 3D volume into six seamless cube faces — for skyboxes, planetary surfaces, nebulae, and stars — on both the WebGL2 and WebGPU backends.
Two renderers turn a volume into cube faces, differing only in how they show the field:
renderCubemapSurface— the field’s raw true color, sampled along each face ray (no lighting, no gamma). Same dynamic range as the field’s 2D view.renderCubemap3d— a lit ``render3d``-style solid (isosurface or voxel, with shading and gamma).
Note
Work in progress — not yet ready for use. Cubemap support is being landed in the engine as a foundational layer for upcoming feature development. The API, parameters, and output are subject to change.
How It Works¶
A cube camera sits at the center of the volume and looks outward through a 90-degree frustum, once per face. The six faces use the axis-aligned directions, in this order:
index 0 +X
index 1 -X
index 2 +Y
index 3 -Y
index 4 +Z
index 5 -Z
Each output pixel becomes a 3D view ray, and the ray marches the volume. Because adjacent faces evaluate their shared edge from the same 3D direction, the edges match exactly — the seams are correct by construction, not by tiling 2D textures. Continuity across every face edge is proven in test/cubeCamera.test.js (closed-cube invariant) and test/cubeExport.test.js (cross-layout adjacency).
Generating Cube Faces¶
Two cubemap renderers take a 3D volume and render the current cube face; drive
either from a 3D generator such as noise3d. They differ in how they show the
field:
Renderer |
What it shows |
|---|---|
|
The raw, true color of the field, sampled along the face normal (front-to-back emission/absorption). No lighting, no gamma — the same dynamic range as the field’s 2D view. Use this to see the field as-is. |
|
The lit “blob in space” — a multi-face clone of |
search synth3d, filter3d, render
noise3d(volumeSize: x64)
.renderCubemapSurface()
.write(o0)
render(o0)
volumeSize (on the generator) sets the volume resolution: x16, x32, x64, or x128 (16³ … 128³).
renderCubemapSurface parameters¶
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
number (0–20) |
4 |
Scales the field’s contribution to per-step opacity |
|
number (0–4) |
1 |
How strongly the medium attenuates along the ray |
|
number (0–4) |
1 |
How much each sample emits |
|
color / number |
|
Background behind the volume |
renderCubemap3d parameters¶
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Smooth isosurface raymarch, or blocky voxel (DDA) traversal |
|
number (0–1) |
0.5 |
Field cutoff the surface is traced to |
|
boolean |
false |
Flip the inside/outside test |
|
color / number |
|
Background behind the volume |
Examples¶
// Raw field, denser
search synth3d, filter3d, render
noise3d(volumeSize: x64).renderCubemapSurface(density: 8, emission: 2).write(o0)
render(o0)
// Lit isosurface "planet shell"
search synth3d, filter3d, render
noise3d(volumeSize: x64).renderCubemap3d(threshold: 0.55).write(o0)
render(o0)
Rendering All Six Faces¶
A cubemap renderer renders one face at a time (whichever cubeBasis the driver sets). To produce all six faces, call renderCubemap() on the renderer (or pipeline). It runs the compiled graph six times — once per face — and returns six pixel buffers. The render style is whichever cubemap renderer the graph ends in (renderCubemapSurface / renderCubemap3d) — not a driver option.
const faces = await renderer.renderCubemap({
size: 512, // face edge length in pixels
outputSurface: 'o0', // the surface the DSL writes to
})
// faces: 6 × { width, height, data: Uint8Array } (RGBA8), in +X,-X,+Y,-Y,+Z,-Z order
Option |
Type |
Default |
Description |
|---|---|---|---|
|
number |
512 |
Face edge length in pixels (the graph is rendered at |
|
string |
|
The user surface ( |
|
number |
0 |
Time value passed to the render (for animated volumes) |
The graph must terminate in a cubemap renderer writing to outputSurface. outputSurface must name a real surface the DSL wrote to; an unknown name throws. A flat 2D chain (no cubemap renderer) would render the same image six times.
Exporting¶
The six faces use these canonical names, in face order:
px.png nx.png py.png ny.png pz.png nz.png (= +X,-X,+Y,-Y,+Z,-Z)
Two pure helpers live in shaders/src/renderer/cubeExport.js:
faceFileNames()→ the six.pngnames above.crossLayout(faces)→ a single{ width, height, data }RGBA8 buffer arranging the faces into a seam-continuous horizontal cross (4×3).
Note
cubeExport.js helpers are not part of the core bundle (like the UI
components). Import them from source, or assemble/name the faces yourself
using the face order above. PNG encoding happens at the call site.
Host Integration¶
For application developers saving the six faces. See Shader Pipeline Integration for renderer setup; the cubemap-specific flow is:
// 1. Compile a graph that ends in a cubemap renderer, then pause the render loop
// so the driver owns the per-face camera while it bakes.
await renderer.loadEffects(['synth3d/noise3d', 'render/renderCubemapSurface'])
await renderer.compile(`
search synth3d, filter3d, render
noise3d(volumeSize: x64).renderCubemapSurface().write(o0)
render(o0)
`)
renderer.stop()
// 2. Render all six faces.
const faces = await renderer.renderCubemap({ size: 1024 })
// 3. Encode each face to a PNG blob in the browser.
async function faceToPng(face) {
const canvas = new OffscreenCanvas(face.width, face.height)
const ctx = canvas.getContext('2d')
ctx.putImageData(new ImageData(new Uint8ClampedArray(face.data), face.width, face.height), 0, 0)
return canvas.convertToBlob({ type: 'image/png' })
}
const names = ['px', 'nx', 'py', 'ny', 'pz', 'nz']
const blobs = await Promise.all(faces.map(faceToPng))
// → save blobs[i] as `${names[i]}.png`, or zip them, or upload.
renderer.start() // resume the live preview
The returned array and its buffers are reused on the next renderCubemap() call — copy a face’s data if you need to retain it across calls.
Technical Notes¶
Face order is fixed:
+X, -X, +Y, -Y, +Z, -Z(indices 0–5), consistent across the camera, driver, export names, and cross layout.Readback works on both backends.
renderCubemapreads the offscreen output surface directly (viacopyTextureToBufferon WebGPU), which sidesteps the canvas IOSurface readback race that affects on-screen captures.Pixel rows are top-down (
readPixelsflips WebGL2’s bottom-up rows to match WebGPU). The exported PNGs and the cross are in standard top-down image orientation.Volume size limits:
x16–x128(128³ is the current ceiling).outputSurfacedefaults too0and must match the surface the DSL writes to.