@plasius/gpu-lighting
v0.2.15
Published
Advanced lighting WGSL modules and planning profiles for @plasius/gpu-worker.
Downloads
420
Maintainers
Readme
@plasius/gpu-lighting
Advanced lighting WGSL modules and planning profiles for @plasius/gpu-worker.
The package is structured around modern lighting tracks:
- Lumen-inspired hybrid realtime GI/reflections.
- Path-traced reference lighting.
- Froxel-based volumetric lighting.
- HDRI/IBL precomputation passes.
Apache-2.0. ESM + CJS builds. WGSL assets are published in dist/.
Install
npm install @plasius/gpu-lightingBrowser Demo
npm run demoThen open http://localhost:8000/gpu-lighting/demo/.
The browser demo now mounts the shared 3D harbor validation scene from
@plasius/gpu-shared instead of a catalog-only page, so lighting-band behavior
is visible against GLTF ships, water, and cloth.
For browser-only serving, the demo resolves @plasius/gpu-shared through an
import map so the page stays on the published package surface rather than a
package-private source path.
Screenshot Capture
The repo also carries the Eames-chair environment validation harness under
demo/eames-environments/ plus tracked Playwright helpers under
scripts/eames-environments/. The referenced chair asset is tracked under
data/models/eames-lounge-chair-ottoman/ so fresh repo checkouts can run the
validation page without depending on a parent monorepo checkout. Build
gpu-performance, gpu-renderer, and gpu-lighting first, then run:
node scripts/eames-environments/capture.mjsQuick-mode captures now include the Eames matrix plus four synthetic reference
scenes: furnace, all-material-direct-light, hdri-skybox, and
dark-terminal-residual. The generated summary.md records which artifact
classes each synthetic scene is intended to catch. To open a specific scene in
the browser harness, pass validationScene=<scene-id> in the page query
string.
For reverse-pass black-pixel diagnostics, run:
node scripts/eames-environments/path-debug-capture.mjsIf fresh Playwright Chromium bootstrap is unstable on macOS, start a
WebGPU-capable Chrome separately with remote debugging enabled and set
PLASIUS_CAPTURE_CDP_URL=http://127.0.0.1:<port> before running the capture
script. The validation page now reports bootstrap step, detail, and WebGPU
availability through window.__plasiusCaptureState and
window.__plasiusCaptureError so capture failures stop at a named phase rather
than hanging on the initial HUD.
For long-running reference captures, browser boot readiness is considered
complete once the page reaches frame rendering; the capture script then waits on
the final render result with an offline timeout budget. This prevents a valid
4K/high-SPP render from being reported as a boot failure while GPU work is still
making progress. The Eames validation page accepts maxDepth values up to 32
for reference-depth experiments.
For browser-controlled fallbacks, start
node scripts/eames-environments/capture-bridge-server.mjs <port> and open the
validation page with captureBitmap=1 plus
captureUploadPath=output/playwright/eames-environments/<name>.png. If the page
is being served by a plain static server such as python -m http.server, also
pass
captureUploadUrl=http://127.0.0.1:<port>/__plasius-capture. The page will
freeze its own canvas and POST the PNG back to the bridge server once the
render completes. The browser-side upload helper now rejects non-loopback
capture endpoints so this fallback cannot be redirected at arbitrary remote
origins.
The main capture and reverse-pass debug capture entry points now share the same server-selection helper, so local reuse, fresh static-server startup, and bridge fallback all follow the same port and readiness rules across macOS and Linux.
The capture scripts now pin deterministic validation settings unless explicitly overridden:
PLASIUS_CAPTURE_MAX_DEPTH=8PLASIUS_CAPTURE_SPP=1PLASIUS_CAPTURE_FRAMES=1PLASIUS_CAPTURE_DENOISE=1PLASIUS_CAPTURE_MOTION=0PLASIUS_CAPTURE_FRAME_INDEX=777PLASIUS_CAPTURE_PROBE=1
The main screenshot harness now supports a scripted matrix:
PLASIUS_CAPTURE_MATRIX_MODE=quickkeeps the default one-scenario-per-preset lane for faster local checks.PLASIUS_CAPTURE_MATRIX_MODE=fullexpands into a cross-product over camera presets, SPP values, and denoise states for offline/reference validation.PLASIUS_CAPTURE_CAMERA_PRESETS=reference,wide,PLASIUS_CAPTURE_SPP_MATRIX=1,4,8,32,128, andPLASIUS_CAPTURE_DENOISE_MATRIX=1,0refine the matrix explicitly.
They save canvas-only PNGs plus per-scenario JSON under
output/playwright/eames-environments/, including the capture URL, a repro
command, renderer stats, exact-black/near-black counts, luminance spread, and
quantized color-bucket metrics that act as lightweight texture-presence
signals. The manifest and summary now also retain failure diagnostics instead of
stopping with only terminal output. The validation page now decouples optional
probe readback from the heavy render submission itself, which keeps higher-SPP
screenshot validation more stable.
The validation HUD/result also surfaces renderer transport guardrails, so
completed jobs, dispatch/submission counts, frame time, tracked memory,
queue-overflow, device-loss state, and adapter capability hints travel with the
same JSON evidence used for release validation.
For motion or realtime validation, the page also accepts frameTimeBudgetMs
and will render at least one full-screen sample before adaptively spending the
rest of the per-frame budget on additional SPP passes. The HUD reports
rendered/target spp whenever the budgeted frame lands below the configured
ceiling. When gpu-performance/dist/index.js is available, the Eames harness
also routes that target through a @plasius/gpu-performance quality ladder fed
by gpu-renderer's wavefront adaptive-sampling levels, so the requested SPP
becomes a release-grade ceiling rather than an ungoverned demo-local heuristic.
The Eames loader also preserves authored UVs, decoded base-colour,
metallic-roughness, normal, occlusion, and emissive maps, plus authored glTF
material factors such as clearcoat, sheen colour, specular colour,
transmission, and IOR when present. That keeps the validation scene on the
shared renderer path instead of relying on material-name-specific overrides for
leather, wood, and chrome.
Usage (load one technique)
import {
loadLightingTechniqueJobs,
loadLightingTechniqueWorkerBundle,
getLightingTechnique,
} from "@plasius/gpu-lighting";
import { assembleWorkerWgsl, loadWorkerWgsl } from "@plasius/gpu-worker";
const workerWgsl = await loadWorkerWgsl();
const { preludeWgsl, jobs } = await loadLightingTechniqueJobs("hybrid");
const shaderCode = await assembleWorkerWgsl(workerWgsl, {
preludeWgsl,
jobs,
});
console.log(getLightingTechnique("hybrid").description);Usage (worker governance bundle)
import {
getLightingProfileWorkerManifest,
loadLightingTechniqueWorkerBundle,
} from "@plasius/gpu-lighting";
const bundle = await loadLightingTechniqueWorkerBundle("hybrid");
// WGSL for gpu-worker assembly
console.log(bundle.preludeWgsl, bundle.jobs);
// Contract-aligned metadata for gpu-performance and gpu-debug integrations
console.log(bundle.workerManifest.jobs[0].performance.levels);
console.log(bundle.workerManifest.jobs[0].debug);
console.log(bundle.workerManifest.schedulerMode);
console.log(bundle.workerManifest.jobs[0].worker.priority);
console.log(bundle.workerManifest.jobs[0].worker.dependencies);
const profileManifest = getLightingProfileWorkerManifest("realtime");
console.log(profileManifest.jobs.map((job) => job.worker.jobType));Wavefront Lighting Contracts
@plasius/gpu-lighting now also publishes a renderer-aligned wavefront
technique for the first active-ray lighting slice. It keeps the queue layout,
buffer contract names, and terminal-hit policy aligned with
@plasius/gpu-renderer while owning the lighting-specific WGSL for terminal
radiance accumulation and continuation scattering.
import {
createWavefrontLightingPlan,
createWavefrontReferenceFixture,
createWavefrontVisibilityProbeRay,
evaluateWavefrontMaterialReference,
evaluateWavefrontTerminalRadiance,
evaluateWavefrontVisibilityProbe,
loadLightingTechniqueWorkerBundle,
} from "@plasius/gpu-lighting";
const plan = createWavefrontLightingPlan({
maxDepth: 6,
queueCapacity: 4096,
explicitLightSampling: true,
visibilityProbeMode: "mis-balanced",
});
const bundle = await loadLightingTechniqueWorkerBundle("wavefront");
const emissive = evaluateWavefrontTerminalRadiance({
hitType: "emissive",
throughput: [0.5, 0.5, 0.5],
emission: [8, 6, 4],
});
const material = evaluateWavefrontMaterialReference({
hitType: "surface",
eventKind: "refraction",
throughput: [1, 1, 1],
transmission: [0.92, 0.95, 0.98],
ior: 1.45,
shadingNormal: [0, 1, 0],
viewDirection: [0, 1, 0],
currentMediumRefId: 0,
surfaceMediumRefId: 7,
});
const probeRay = createWavefrontVisibilityProbeRay({
rayId: 12,
parentRayId: 4,
sourcePixelId: 9,
sampleId: 2,
bounce: 1,
origin: [0, 1, 0],
direction: [0.25, -1, 0.15],
throughput: [0.8, 0.7, 0.6],
mediumRefId: 7,
mediumStack: [7],
});
const probe = evaluateWavefrontVisibilityProbe({
probeRay,
probeMode: "exclusive-emissive",
activeEmissiveRadiance: [4, 3, 2],
emissiveRadiance: [4, 3, 2],
transparentSegments: [[0.8, 0.8, 0.8]],
});
const fixture = createWavefrontReferenceFixture({
hitType: "emissive",
throughput: [0.8, 0.7, 0.6],
emission: [4, 3, 2],
visibilityProbe: {
probeMode: "mis-balanced",
emissiveRadiance: [0.6, 0.3, 0.1],
transparentSegments: [[0.7, 0.8, 0.9]],
},
});
console.log(plan.requiredRendererPassOrder);
console.log(plan.visibilityProbeMode);
console.log(bundle.jobs.map((job) => job.label));
console.log(emissive.radiance);
console.log(material.continuation.mediumState);
console.log(probe.doubleCountPrevented);
console.log(fixture.tolerance);This slice keeps emissive hits, environment hits, and environment-miss dark fallbacks on the lighting package surface without reintroducing a depth-first shader dependency into the renderer-owned wavefront queue model.
The continuation/reference surface now also exposes:
- compact medium-state carry for refraction/transparency events, including total-internal-reflection fallback reporting
- shared-ray payload helpers where visibility probes reuse the base ray layout
and encode their kind through the low bits of
flags - optional probe contribution helpers with
mis-balancedandexclusive-emissivemodes so active emissive hits remain correct even when explicit light sampling is enabled - deterministic reference fixtures that publish buffer-like accumulation outputs
with a documented default tolerance of
0.0005for CPU-vs-GPU comparisons
Distance-Banded Lighting
import { createLightingBandPlan } from "@plasius/gpu-lighting";
const bandPlan = createLightingBandPlan({
profile: "realtime",
importance: "high",
});
console.log(bandPlan.bands.map((band) => band.primaryShadowSource));
console.log(bandPlan.bands.find((band) => band.band === "near").rtParticipation);Band plans make near, mid, far, and horizon shadow sources explicit, keep RT shadow/reflection/GI participation independent, and publish temporal reuse plus update cadence expectations for downstream renderer and performance packages.
Environment Lighting Presets
import {
createEnvironmentLightingConfig,
createWavefrontEnvironmentLightingOptions,
} from "@plasius/gpu-lighting";
const lighting = createEnvironmentLightingConfig({
scene: "forest",
timeOfDay: "dusk",
intensity: 1.05,
environmentPortals: [
{
id: "north-window",
position: [0, 1.2, -2.4],
normal: [0, 0, 1],
tangent: [1, 0, 0],
width: 1.8,
height: 1.1,
intensity: 1.4,
},
],
});
const wavefrontLighting = createWavefrontEnvironmentLightingOptions({
preset: "cavern-night",
});
console.log(lighting.environmentLightSources.map((source) => source.kind));
console.log(wavefrontLighting.environmentMissLighting.startingPoint);createEnvironmentLightingConfig(...) owns the reusable sky/environment
semantics: horizon and zenith colours, key-light direction, key-light colour,
environment intensity, exposure, ambient residual colour, and optional
environment-light portals. The grass-field, forest, warehouse, and cavern
families use restrained ambient residual scaling so low-sample renderers keep
some final-bounce colour without washing dark materials toward white. They also
publish sunlitBaseline, a scene-scaled time-of-day daylight floor that
renderers can use at terminal path collisions without raising the global
ambient colour.
Preset families now cover:
grass-field-{dawn,midday,dusk,night}forest-{dawn,midday,dusk,night}warehouse-{dawn,midday,dusk,night}cavern-{dawn,midday,dusk,night}
Callers can pass the combined preset name directly or pass scene plus
timeOfDay; scene-only aliases default to midday.
Each preset publishes scene, timeOfDay, normalized
sunlitBaseline, environmentLightSources, a dominantLightSource, and
environmentMissLighting. Source metadata includes source kind, role, direction,
position, colour, intensity, radiance, luminance, reach, and angular radius.
Renderers can use environmentMissLighting when a path ray misses scene
geometry: the miss has an inferred source colour/brightness and a stable
startingPoint of environment-miss instead of an unbounded null/negative sky
sample. Emissive material hits remain explicit light-source hits and should not
be double-counted by environment inference. Callers can also pass an
environmentMap/hdri descriptor; the lighting config preserves it in
createWavefrontEnvironmentLightingOptions(...) so the wavefront renderer can
sample an equirectangular radiance map for environment misses and ambient
residuals instead of relying primarily on static ambient values.
Portals describe physical openings such as windows where outside radiance can
enter an interior. They are normalized as rectangle apertures with position,
normal, tangent, dimensions, colour, and radiance scale.
createWavefrontEnvironmentLightingOptions(...) projects that contract into the
current @plasius/gpu-renderer wavefront renderer options without making the
renderer depend on this package directly.
DAG Scheduling
Lighting worker manifests now publish schedulerMode: "dag" plus per-job
priority and dependencies so downstream runtimes can preserve stage order.
hybrid:directLightingandscreenTraceare roots;radianceCache,finalGather, andreflectionResolveunlock as upstream work finishes.pathtracer:pathTrace -> accumulate -> denoisevolumetrics:volumetricShadow -> froxelIntegratehdri:irradianceConvolutionandspecularPrefilterare roots;brdfLutjoins after both finish.
Usage (profile planning)
import {
createLightingProfileModeLadder,
getLightingProfile,
loadLightingProfile,
} from "@plasius/gpu-lighting";
const profile = getLightingProfile("realtime");
// profile.techniques -> ["hybrid", "volumetrics", "hdri"]
const plan = await loadLightingProfile("realtime");
// plan.techniques is an array of loaded prelude+job WGSL bundles.
const modeLadder = createLightingProfileModeLadder();
// modeLadder exposes reference -> hybrid -> realtime ordering for gpu-performance.Usage (reference-first performance ladder)
import {
createGpuPerformanceGovernor,
createQualityLadderAdapter,
} from "@plasius/gpu-performance";
import {
createLightingProfileModeLadder,
} from "@plasius/gpu-lighting";
const lightingModePlan = createLightingProfileModeLadder({
initialProfile: "reference",
});
const lightingMode = createQualityLadderAdapter(lightingModePlan);
const governor = createGpuPerformanceGovernor({
device,
modules: [lightingMode],
target: lightingModePlan.target,
adaptation: lightingModePlan.adaptation,
});createLightingProfileModeLadder() publishes the policy contract for the
reference-first mode you described:
- start from
reference - keep a 4-frame adaptation window
- hold the premium mode while the negotiated average remains at or above
30FPS - degrade the whole lighting profile to
hybrid, thenrealtime, only when that window can no longer sustain the budget
The package now ships concrete WGSL contracts for:
hybrid.directLighting: direct sun/sky resolve with roughness-aware specular shapinghybrid.screenTrace: first-hit reflection tracing over the shared hybrid scene contractshybrid.radianceCache: irradiance history updates for cache-backed indirect reusehybrid.finalGather: cache + trace composition with temporal reuse for the hybrid GI pathvolumetrics.volumetricShadow: slice-aware Beer-Lambert shadow history for fog and shaftsvolumetrics.froxelIntegrate: froxel scattering/extinction integration with temporal stabilityhdri.irradianceConvolution: cosine-weighted diffuse environment convolutionhdri.specularPrefilter: roughness-aware environment prefiltering for glossy IBLhdri.brdfLut: split-sum BRDF LUT integration for image-based lightingpathtracer.pathTrace: analytic scene tracing, bounce integration, and sky fallbackpathtracer.accumulate: progressive history resolve with reset handlingpathtracer.denoise: spatial-temporal bilateral filtering for reference previewshybrid.reflectionResolve: surface-aware reflection shading with roughness/fresnel shaping
This is still a catalog/planning package rather than proof of a finished
end-to-end renderer. Downstream runtimes such as @plasius/gpu-renderer still
need to bind real scene buffers and execute these kernels on the live frame
graph.
Profiles
realtime: Lumen-inspired hybrid GI/reflections + volumetrics + HDRI/IBL.hybrid: hybrid GI/reflections with HDRI/IBL support.reference: path tracing + volumetrics + HDRI/IBL for validation and lookdev.
Techniques
hybriddirectLightingscreenTraceradianceCachefinalGatherreflectionResolve
pathtracerpathTraceaccumulatedenoise
volumetricsfroxelIntegrate: accumulates participating-media scattering/extinction per froxelvolumetricShadow: resolves directional shadow transmittance history per froxel
hdriirradianceConvolution: builds diffuse irradiance from the environment sourcespecularPrefilter: builds roughness-aware glossy environment mip databrdfLut: integrates the split-sum BRDF lookup surface for IBL
Demo
Run the demo server from the repo root:
cd gpu-lighting
npm run demoThen open http://localhost:8000/gpu-lighting/demo/.
The mounted 3D scene keeps the lighting profile, band-policy, and worker-state
catalog visible while rendering the shared harbor validation surface.
Development Checks
npm run lint
npm run typecheck
npm run test:coverage
npm run build
npm run pack:check
npm run zero-three
npm run zero-three:testZero-Three architecture invariant
This GPU-native package permanently prohibits Three.js and every package whose dependency, peer, or optional graph reaches it. The prohibition covers source, public declarations, tests, tooling, manifests, lockfiles, installed graphs, bundles, npm tarballs, SBOMs, and active documentation. There is no compatibility mode, waiver, or renderer fallback.
Run npm run zero-three:source before installation and npm run zero-three
after building to generate the immutable package evidence consumed by site
release-integrity validation. npm run zero-three:test exercises the fail-closed
negative fixtures. The system-wide decision is recorded in
ADR 0168.
Files
src/index.js: technique/profile catalogs, loader APIs, validation.src/techniques/hybrid/*: realtime hybrid GI/reflections WGSL modules.src/techniques/pathtracer/*: path tracing reference WGSL modules.src/techniques/volumetrics/*: volumetric lighting WGSL modules.src/techniques/hdri/*: HDRI/IBL precompute WGSL modules.docs/adrs/*: architecture decisions for the lighting stack.docs/tdrs/*: technical design records for worker manifests and debug hooks.docs/design/*: integration guidance for worker budgets, DAG metadata, and debug instrumentation.
Release integrity
CI keeps the administrative contributor registry outside Git and npm package artifacts using exact, case-normalised path checks. CI runs on approved GitHub-hosted runners. Release preparation and publication use a two-run exact-main protocol on GitHub-hosted Node.js 24.18.0 LTS with a pinned npm 11.6.2 release client. A read-only job seals the package tarball, SBOM, and Zero-Three evidence before a dependency-free production job publishes that exact artifact through npm OIDC with provenance; there is no npm write-token fallback.
