AURA

JSGG

AuraJS
DOCSEXAMPLESGITHUB
08 Draw3D, Camera, Lighting, Shadows, and PostFX
3D drawing, camera setup, lighting, shadows, and post-processing surfaces.
docs/external/game-dev-api/08-draw3d-camera-lighting-shadows-and-postfx.md

Draw3D, Camera, Lighting, Shadows, and PostFX

These are the main real-time 3D submission and render-control surfaces.

`aura.draw3d`

Primary 3D drawing/control methods:

  • drawMesh(meshHandle, materialHandle, transformOrNode)
  • billboard(textureHandleOrSource, options)
  • drawSkybox(textureHandleOrSpec)
  • setEnvironmentMap(pathOrCubeCameraOrNull)
  • clear3d(color)
  • setFog(fogSpec)
  • setAtmosphereProfile(profileOrNull, overrides?)
  • clearFog()
  • setOcclusionCulling(enabledOrOptions)
  • addDecal(options)
  • createCubeCamera(options?)
  • updateCubeCamera(cubeCameraHandle)
  • destroyCubeCamera(cubeCameraHandle)
  • createRenderTarget(width, height, options?)
  • setRenderTarget(renderTargetHandleOrNull, captureCamera?)
  • destroyRenderTarget(renderTargetHandle)
  • createLine(options)
  • createLineSegments(options)
  • updateLine(lineHandle, patch)
  • destroyLine(lineHandle)
  • clearStencil(value?)

PostFX lives here today, not in a separate namespace:

  • setPostFXPass(name, options)
  • setPostFXEnabled(name, enabled)
  • removePostFXPass(name)
  • getPostFXState()
  • registerPostFXShader(name, wgslSource)

Stage 180 landed stock named postFX presets on top of this same seam rather than creating a separate post-processing namespace.

Fog, Atmosphere V1, and Atmosphere Profiles

The retained fog seam still starts at setFog(...), and Atmosphere V1 still layers onto that same entry point instead of introducing a second renderer namespace.

Stage 195 adds one thin authored helper on top of that retained floor:

  • setAtmosphereProfile(profileOrNull, overrides?)

Built-in profiles today:

  • neon-night
  • storm-haze

The helper is intentionally narrow. It only composes:

  • retained setSkyGradient(...)
  • retained setFog({ ..., atmosphere })

It does not own drawSkybox(...), setEnvironmentMap(...), local fog volumes, or any deeper volumetric system.

Example:

aura.draw3d.setAtmosphereProfile('neon-night', {
  density: 0.045,
});

Truthful boundaries:

  • the base fog contract remains global retained fog on the main forward 3D scene pass
  • the profile helper is an authored preset layer over retained sky gradient + retained fog, not a new atmospheric renderer
  • Atmosphere V1 adds one bounded height-aware fog overlay plus one renderer- owned screen-space shaft lane
  • the shaft direction comes from the active directional light; with no directional light, the height-fog overlay can still run but shafts fall away
  • the atmosphere overlay is perspective-camera-only
  • this is not true volumetric fog, not local fog volumes, and not a per-object fog-exclusion system
  • the overlay does not promise cube-camera capture, render-target capture, or other non-swapchain paths
  • the helper owns retained sky-gradient state only; it does not imply an asset-backed skybox or environment-map preset lane

If you are using the helper, prefer:

aura.draw3d.setAtmosphereProfile(null);

That keeps the profile-owned sky gradient and the retained fog/Atmosphere V1 state coherent.

clearFog() still remains the lower-level seam that clears the retained fog state and the coupled Atmosphere V1 overlay only.

Cube-camera truth

Stage 191 closes one bounded live cube-camera capture lane on top of the existing command surface.

Public seam:

  • createCubeCamera({ position?, near?, far?, resolution?, facesPerFrame? })
  • updateCubeCamera(cubeCamera)
  • destroyCubeCamera(cubeCamera)
  • setEnvironmentMap(cubeCamera) to use the captured cubemap as the shared live IBL source

Runtime and cost truth:

  • capture is explicit and manual, not an automatic every-frame reflection pass
  • new cube cameras start dirty, and updateCubeCamera(...) or position changes mark all six faces dirty again
  • facesPerFrame clamps to 1..6 and defaults to 6
  • the renderer refreshes faces round-robin, so facesPerFrame: 2 settles over three frames instead of forcing all six faces in one frame
  • this remains one shared scene-level live IBL source by default, with one bounded per-material cube-camera override now available on aura.material.setCubeMapTexture(...)

Current explicit boundaries:

  • aura.material.setCubeMapTexture(material, cubeCameraOrNull) now ships one bounded per-material override over the same cube-camera source lane; null clears back to the shared scene IBL source
  • cube-camera capture covers the skybox/gradient plus queued forward mesh draws
  • pending cube-camera faces can refresh in the same frame as either the swapchain main scene or a whole-frame render-target capture, because that cube-camera pass runs ahead of the main 3D scene pass
  • the capture path does not currently include postFX, skinned meshes, terrain, billboards, decals, retained 3D lines, shadow passes, HiZ occlusion, or stencil-on-capture behavior
  • per-material cube-camera overrides currently bind only on the main forward-material consumer path rather than the cube-camera capture path or skinned-mesh path
  • while faces are being refreshed, the capture pass uses fallback IBL rather than recursively sampling the in-flight cube camera

Example:

const cubeCam = aura.draw3d.createCubeCamera({
  position: { x: 0, y: 2.5, z: 0 },
  resolution: 256,
  facesPerFrame: 2,
});

aura.draw3d.setEnvironmentMap(cubeCam);

// Optional per-material override over the same live cube-camera source.
aura.material.setCubeMapTexture(chromeMaterial, cubeCam);

// Refresh the shared live IBL source after moving the probe.
aura.draw3d.updateCubeCamera(cubeCam);

// Revert one material back to the shared scene IBL source.
aura.material.setCubeMapTexture(chromeMaterial, null);

Custom postFX contract

Custom WGSL postFX now has one explicit truthful input contract:

  • @binding(0) input_texture: texture_2d<f32> current scene color immediately before this custom pass
  • @binding(1) input_sampler: sampler shared filtering sampler for scene-color reads
  • @binding(2) u_postfx resolution, texel size, stock params, time, 8 custom param slots, camera near/far, projection mode, and step/input-contract flags
  • @binding(3) depth_sampler: sampler_comparison comparison sampler for depth-aware reads
  • @binding(4) depth_texture: texture_depth_2d scene depth from the current swapchain 3D pass
  • @binding(5) original_scene_texture: texture_2d<f32> stable scene-color copy from before the postfx chain started

Truthful boundaries:

  • custom shaders can now compare the current pass input against the original pre-postfx scene color
  • custom shaders can read scene depth, and the uniform now includes camera near/far plus an orthographic flag so depth interpretation is no longer guesswork
  • there is still no normal buffer / G-buffer exposure
  • there is still no arbitrary intermediate-buffer readback or user-defined multi-input render graph

aura.draw3d.getPostFXState() now also reports:

  • resolvedSteps for the deterministic runtime execution order
  • customShaderBindings for the public binding map
  • customShaderContract for the supported input semantics and remaining limits

Stage 213 also ships one bounded retained postFX-graph owner on top of this same floor:

  • createPostFXGraph({ enabled?, passes, targetChain? })
  • updatePostFXGraph(graph, patch?)
  • destroyPostFXGraph(graph)
  • getPostFXGraphState(graph?)

Current truthful graph semantics:

  • one graph owns an ordered linear pass chain with between 1 and 4 pass entries
  • one graph owns at most 2 named intermediate targets through one shared targetChain
  • pass order inside the graph preserves the authored array order
  • last enabled graph wins
  • fallback is explicit clear, not "use the previous graph"
  • while any retained postFX graph exists, low-level composer mutation through setPostFXPass(...), setPostFXEnabled(...), and removePostFXPass(...) is outside the mixed-ownership lane and currently rejects with postfx_invalid_options
  • destroying the last graph clears graph-owned composer state instead of restoring an earlier manual low-level pass stack

This remains intentionally narrow:

  • it does not claim per-pass target-chain overrides
  • it does not claim graph blending or graph branches
  • it does not claim a general render graph, a G-buffer lane, or a new aura.postfx namespace

Stock outline postFX

Stage 191 adds one stock global outline lane on top of the existing postFX composer seam:

aura.draw3d.setPostFXPass('outline', {
  strength: 0.82,
  radius: 1.5,
  threshold: 0.35,
  customParams: {
    depthThreshold: 0.018,
    lumaThreshold: 0.08,
    outlineR: 0.08,
    outlineG: 0.95,
    outlineB: 1.0,
  },
});

Truthful boundaries:

  • this is a screen-space full-frame stylized edge pass, not geometry extrusion or per-material toon outlining
  • the pass reads scene color plus depth, and it may use the stable original_scene_texture copy to keep edge detection from drifting after earlier postFX
  • the public tuning knobs are strength, radius, threshold, depthThreshold, lumaThreshold, and outlineR/G/B
  • it does not expose normals or a G-buffer, so extremely subtle coplanar edges still depend on depth and luminance contrast rather than true normal-based edge detection

`particles3d` runtime truth on Draw3D

aura.particles3d.draw() currently renders through the same billboard/material submission seam described below rather than through a separate GPU-simulated particle pipeline.

Current truthful runtime behavior:

  • retained emitters now have explicit lifecycle controls through create/destroy/emit/burst/pause/resume/stop/getState
  • one-shot burst(options) emitters auto-clean after their particles empty
  • draw() submits billboard-backed particle draws through the shared Draw3D billboard lane, so the accepted render paths are:
    • the main swapchain 3D scene path
    • whole-frame 3D render-target capture when setRenderTarget(handle, ...) stays active for that frame, including the alternate capture-camera form
  • aura.debug.inspectorStats() now includes scene3dRuntime.particles3d so particle counts, draw calls, and lifecycle state do not stay opaque

Current postfx truth:

  • the bounded proof lane is the main swapchain scene where particle billboards submit before the standard full-frame postfx composer executes
  • this is now covered by focused runtime proof for the shared draw3d plus particles3d seam
  • when a 3D render target is active for the whole frame, particles still ride the same billboard submission path, but postfx is explicitly skipped for that capture
  • cube-camera refresh can coexist with those accepted main-scene paths, but cube-camera capture itself still excludes billboard-backed particles
  • the canonical bounded example for this lane is examples/neon-district, where fx.js pairs the neon-night atmosphere profile with the authored monitor capture prop
  • deeper compatibility claims for SSR, DOF, motion blur, SSAO, dual-render split passes, or cube-camera capture remain later follow-on work rather than a promised fully integrated particle render graph

Billboard seam

aura.draw3d.billboard(...) is the lightweight camera-facing quad path for actors, effect cards, and simple in-world screens.

Truthful texture-source inputs today:

  • numeric handle from an already-resolved material or video texture lane
  • { dataTextureHandle } bridge object for procedural billboards backed by aura.material.createDataTexture(...)

The bridge object exists because data textures and materials do not share one safe public handle namespace. Use the bridge form for procedural/no-asset billboards instead of guessing that a bare overlapping numeric handle will resolve the way you intended.

`particles3d` runtime truth

aura.particles3d.draw() currently uses aura.draw3d.billboard(...) on the shared scene3d runtime path.

Focused truth after Stage 187:

  • the public lifecycle seam now includes emit, pause, resume, stop, draw, and getState
  • focused proof now covers:
    • lifecycle ownership
    • normal same-frame swapchain postfx execution
    • whole-frame 3D render-target capture without postfx
  • this is not a dedicated particle-compositing path and does not yet promise bespoke compatibility guarantees for SSR, SSAO, DOF, motion blur, mid-frame dual-render splits, or cube-camera capture

Decal seam

aura.draw3d.addDecal(options) is currently a narrow detail-overlay seam. The shipped renderer path submits oriented decal quads / decal plane batches that you place explicitly in the world.

Use it for authored surface detail such as signage overlays, puddle-edge accents, grime cards, scorch marks, or graffiti planes.

Accepted authored albedo sources today:

  • texture: 'path/to/decal.png'
  • textureHandle: aura.material.createDataTexture(...)
  • color: { r, g, b, a? } for a flat-color fallback or tint

You can combine color with either texture source as a tint, but not with both texture and textureHandle at the same time.

Current geometry truth:

  • the active renderer path resolves one oriented overlay plane per authored decal command
  • size.x / size.z define the visible overlay footprint
  • size.y stays part of the authored payload for future projected-volume compatibility, but it does not currently widen the live overlay mesh into a true receiver-projected decal volume

It does not currently guarantee true projected decals onto arbitrary scene geometry. In particular, it is not a mesh-conforming deferred decal pass that automatically wraps across mixed receiver topology.

3D render-target capture seam

aura.draw3d.createRenderTarget(...) plus aura.draw3d.setRenderTarget(renderTargetHandleOrNull, captureCamera?) is the explicit offscreen full-scene 3D capture lane.

Current truthful modes:

  • setRenderTarget(handle) redirects the current frame's full 3D scene pass into that render target using the current camera state
  • setRenderTarget(handle, { position, target, up?, fovDegrees?, near?, far? }) redirects that full 3D scene pass into the target from an alternate perspective camera
  • setRenderTarget(null) restores rendering to the swapchain

Current explicit boundaries:

  • this is an offscreen full-scene capture seam, not an automatic dual-render CCTV or picture-in-picture pass that also draws a second copy to the swapchain in the same call
  • setRenderTarget(...) is a frame-latched redirect for the current 3D scene pass, not a mid-frame per-command split graph
  • the capture uses the render target's aspect ratio rather than the window aspect ratio
  • current postfx, HiZ occlusion, and stencil-on-render-target behavior remain outside this seam and stay swapchain-only
  • billboard-backed particles3d draws participate in this whole-frame capture path because they submit on the same Draw3D billboard lane
  • pending cube-camera faces can still refresh before this main-scene capture path, but that does not create a second billboard/postfx render path on the cube-camera side

`aura.camera3d`

Projection, transform, and control helpers:

  • perspective(fov, near, far)
  • orthographic(sizeOrBounds, near, far)
  • setProjectionMode(mode)
  • setOrthoSize(size)
  • getProjectionMode()
  • lookAt(x, y, z)
  • setPosition(x, y, z)
  • setTarget(x, y, z)
  • setFOV(fov)
  • getViewMatrix()
  • getProjectionMatrix()
  • setControlProfile(profile, options)
  • updateControls(dt, inputState)
  • getControlState()

Use cases:

  • scripted cameras
  • orbit/editor-style controls
  • view/projection extraction for custom logic

`aura.light`

Light creation and updates:

  • ambient(color, intensity?)
  • hemisphere(skyColor, groundColor, intensity?, upDirection?)
  • directional(direction, color, intensity?)
  • point(position, color, intensity?, range?)
  • spot(position, direction, color, intensity?, range?, angle?)
  • update(lightHandle, patch)
  • remove(lightHandle)

Shadow control also lives here today:

  • configureDirectionalShadows(options)
  • configureShadow(lightHandle, options)
  • getShadowState()
  • getShadowStats()
  • setShadowCasting(nodeOrHandle, enabled)
  • setShadowQuality(levelOrOptions)
  • setShadowBudget(options)

Important note:

  • There is no separate aura.shadow namespace today.
  • If you are building tooling or agents, treat shadows as part of aura.light.

Reality Check: High-Impact 3D Features Already Ship

If you are planning "AAA next steps", do not treat the following as missing engine features in native AuraJS:

  • stock postFX presets now ship on top of the existing custom WGSL seam
  • a stock global outline postFX pass now ships on the same seam
  • custom WGSL postFX registration already exists
  • spot lights and hemisphere fill lighting already exist
  • IBL/environment maps already exist
  • one bounded shared live cube-camera IBL lane now exists
  • billboards already exist
  • occlusion culling already exists
  • 3D render targets already exist
  • one canonical neon vertical slice now exists to prove multiple shipped surfaces together

Stage 180 also closed the previously drifting seams:

  • stencil is truthful on the swapchain forward path
  • retained 3D lines are truthful as a retained runtime seam
  • decals are a narrow authored-overlay seam (oriented decal quads / planes), not arbitrary projected decals

The remaining frontier is broader renderer breadth, especially true projected decals and wider render-path coverage, not rediscovering the shipped quick wins above.

Stage 180 surfaced the shipped quick wins with copyable snippets and example coverage.

Quick-win starter snippet:

aura.draw3d.setEnvironmentMap('env/cyberpunk.hdr');
aura.draw3d.setOcclusionCulling({ enabled: true, conservative: true });
aura.light.spot(
  { x: 8, y: 6, z: 8 },
  { x: -0.45, y: -1.0, z: -0.25 },
  { r: 0.45, g: 0.78, b: 1.0 },
  1.35,
  28,
  0.72,
);

Material-side follow-up lives on the next page:

  • runtime emissive via aura.material.setEmissive(handle, color, strength?)
  • runtime clearcoat via aura.material.setClearcoat(handle, clearcoat, roughness)
  • denser grid planes via aura.mesh.createPlane(width, depth, widthSegments, depthSegments)
  • sheen via aura.material.create({ sheenColor, sheenRoughness })
  • texture transforms via aura.material.setTextureTransform(...)
  • data textures via aura.material.createDataTexture(...)
  • stencil APIs via aura.material.setStencil(...)

Current truthful boundary:

  • emissive and clearcoat now have a runtime setter seam
  • dense grid planes now stay on the existing createPlane(...) API with extra optional segment counts
  • subsurface remains a later advanced-material task, not part of the shipped runtime parity surface here

Current namespace layout caveats

  • There is no separate aura.postfx namespace today.
  • There is no separate aura.shadow namespace today.
  • aura.scene3d handles hierarchy, imported-scene metadata, clip helpers, and render binding; it does not replace aura.draw3d.drawMesh(...) ownership.

Follow-up page

For scene graph, meshes, materials, glTF import, and skinned meshes, continue to:

DOCUMENT REFERENCE
docs/external/game-dev-api/08-draw3d-camera-lighting-shadows-and-postfx.md
AURAJS
Cmd/Ctrl+K
aurajsgg