@plasius/gpu-renderer 0.2.50 → 0.2.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,5 @@
1
1
  # @plasius/gpu-renderer
2
2
 
3
- ## Native animated showcase surfaces
4
-
5
- `createNativeSceneRenderer({ canvas })` draws interleaved world-space triangles
6
- with depth testing, four-sample antialiasing, filtered directional shadows and
7
- animated water with a planar reflection. Call `render({ vertices, camera, time,
8
- wakes })` from the caller's animation loop and `destroy()` on teardown. Each vertex
9
- has twelve floats: position XYZ, normal XYZ, linear colour RGB, roughness,
10
- metalness, and procedural detail kind (0 plain, 1 wood, 2 mineral/cloth, 3 terrain).
11
- `camera` supplies `eye`, `target` and optional vertical `fov`. At most four wakes
12
- contain `[x, z, headingRadians]`; water can be disabled with `water: false`.
13
-
14
- The input limit defaults to 300,000 vertices. Colour/depth targets are reused and
15
- render dimensions are capped at 3840 per axis and a 1080p pixel budget. Snapshots identify `webgpu-raster`
16
- and count submitted frames; they do not claim ray tracing, GPU completion or
17
- photorealism. Native initialization failures reject; device loss changes
18
- `available` and calls `onUnavailable`. The existing wavefront APIs are unchanged.
19
-
20
- Enable only through the consumer's remote `gpu-demo.scene-fidelity.enabled`
21
- decision and existing access capability. See [design](docs/design/native-showcase-surfaces.md)
22
- and [ADR-0030](docs/adrs/adr-0030-native-showcase-surfaces.md) for rollout and limits.
23
-
24
3
  [![npm version](https://img.shields.io/npm/v/@plasius/gpu-renderer.svg)](https://www.npmjs.com/package/@plasius/gpu-renderer)
25
4
  [![Build Status](https://img.shields.io/github/actions/workflow/status/Plasius-LTD/gpu-renderer/ci.yml?branch=main&label=build&style=flat)](https://github.com/Plasius-LTD/gpu-renderer/actions/workflows/ci.yml)
26
5
  [![coverage](https://img.shields.io/codecov/c/github/Plasius-LTD/gpu-renderer)](https://codecov.io/gh/Plasius-LTD/gpu-renderer)
@@ -29,70 +8,11 @@ and [ADR-0030](docs/adrs/adr-0030-native-showcase-surfaces.md) for rollout and l
29
8
  [![Security Policy](https://img.shields.io/badge/security%20policy-yes-orange.svg)](./SECURITY.md)
30
9
  [![Changelog](https://img.shields.io/badge/changelog-md-blue.svg)](./CHANGELOG.md)
31
10
 
32
- Framework-agnostic WebGPU renderer runtime for Plasius projects.
33
- This package is intended to replace Three.js-dependent render orchestration with
34
- an explicit WebGPU-first runtime that can be consumed from React, vanilla, or
35
- worker-driven app surfaces.
36
-
37
- The package targets Node.js 24, consumes the stable `@plasius/gpu-shared` 1.x
38
- runtime line (`^1.1.1`), and validates its development tooling against the
39
- current ESLint 10 and TypeScript 7 baselines.
40
-
41
- Apache-2.0. ESM + CJS builds.
42
-
43
- ## Privacy-Safe Feedback Diagnostics
44
-
45
- Approved in-game viewers can explicitly convert a small set of current renderer
46
- health observations into the closed
47
- `@plasius/gpu-shared/feedback-diagnostics` packet:
48
-
49
- ```js
50
- import { createFeedbackGameDiagnosticSnapshot } from "@plasius/gpu-renderer";
51
-
52
- const diagnostics = createFeedbackGameDiagnosticSnapshot({
53
- featureEnabled,
54
- capabilityGranted,
55
- consentConfirmed: true,
56
- surfaceId: "site.gpu-demo",
57
- renderer: "webgpu",
58
- backend: "worker",
59
- viewportWidth: 1920,
60
- viewportHeight: 1080,
61
- frameRate: 59.9,
62
- frameTimeMs: 16.7,
63
- featureIds: ["renderer.frame-loop"],
64
- counters: [{ code: "frame-drop", count: 2 }],
65
- errorCodes: [],
66
- });
67
- ```
11
+ Framework-independent WebGPU rendering for browser applications. Render triangle
12
+ meshes with the wavefront path tracer, draw native raster surfaces, or use the
13
+ frame lifecycle and XR integration APIs in your own rendering pipeline.
68
14
 
69
- The helper returns `null` before reading renderer observations unless the
70
- remotely evaluated `feedback.game-diagnostics.enabled` flag, the
71
- `feedback.game-diagnostics.attach` capability, and explicit consent are all
72
- present. It derives trusted provenance and returns only coarse buckets and
73
- closed codes.
74
-
75
- This API is deliberately separate from general renderer snapshots. It accepts
76
- no canvas, user-captured pixels, DOM, player object, URL, filename,
77
- adapter/device detail, coordinate or raw warning, performs no automatic
78
- diagnostic capture or network/storage operation, and never registers
79
- `/player-system`. Disabling the flag preserves ordinary structured-only bug
80
- reporting.
81
-
82
- ## Adaptive per-pixel implementation status
83
-
84
- Per-pixel adaptive rendering is not yet exposed by the public renderer API.
85
- The internal [metadata foundation](docs/design/adaptive-metadata.md) now provides
86
- reflected requested/completed count storage and lazy, bounded allocation.
87
- `@plasius/gpu-shader` validates the final initialization WGSL in development;
88
- the reflector does not enter browser bundles. The normal `npm test` suite checks
89
- generated byte constants, count packing, allocation admission and cleanup.
90
-
91
- The fixed renderer and its whole-frame budget adjustment are unchanged. Actual
92
- completed-count normalization, compact primary scheduling, foveation and shared
93
- integration must land before a site comparison can claim per-pixel adaptation.
94
- No image-quality, performance or net-memory-saving claim follows from the
95
- metadata-only WebGPU execution fixture. See [ADR 0029](docs/adrs/adr-0029-reflected-adaptive-metadata-admission.md).
15
+ Apache-2.0 · ESM and CommonJS · TypeScript declarations included.
96
16
 
97
17
  ## Install
98
18
 
@@ -100,667 +20,149 @@ metadata-only WebGPU execution fixture. See [ADR 0029](docs/adrs/adr-0029-reflec
100
20
  npm install @plasius/gpu-renderer
101
21
  ```
102
22
 
103
- ## Permanent Zero-Three boundary
104
-
105
- This WebGPU renderer and its complete production, development, test, tooling,
106
- peer, optional, and artifact dependency graphs permanently prohibit Three.js,
107
- R3F, TSL, their related packages, and any dependency path reaching them. There
108
- is no compatibility mode, fallback, waiver, or rollback to those renderers.
109
-
110
- Run `npm run zero-three:source` before installing dependencies, or run
111
- `npm run build && npm run zero-three` after `npm ci` for the installed graph,
112
- built bundle, actual npm tarball, and generated CycloneDX evidence. The full
113
- command writes digest-bound package/version evidence to
114
- `release-artifacts/zero-three-evidence.json`. CI retains it, and production CD
115
- passes it by immutable artifact ID, verifies it again, attaches it to the
116
- GitHub release, and attests it.
117
-
118
- ## Usage
23
+ Use Node.js 24 or later for package tooling and a WebGPU-capable browser over
24
+ HTTPS or localhost for rendering. This is a browser GPU runtime; installing it
25
+ in Node does not provide a GPU device. A bundler resolves the package imports.
26
+ Initialization rejects when WebGPU or the requested resources are unavailable;
27
+ your application should present that error and an appropriate retry/fallback UI.
119
28
 
120
- ```js
121
- import { createGpuRenderer } from "@plasius/gpu-renderer";
29
+ ## Quick start: render a mesh
122
30
 
123
- const renderer = await createGpuRenderer({
124
- canvas: document.querySelector("#scene"),
125
- clearColor: "#102035",
126
- });
31
+ Add a canvas to your page:
127
32
 
128
- renderer.resize(window.innerWidth, window.innerHeight);
129
- renderer.start();
33
+ ```html
34
+ <canvas id="scene" width="640" height="360" aria-label="Rendered triangle"></canvas>
130
35
  ```
131
36
 
132
- ## Canonical model ingestion
133
-
134
- `createCanonicalWavefrontMeshInputs` adapts decoded primitive geometry and image
135
- samples from the canonical model pipeline to the renderer's existing
136
- `WavefrontMeshInput` shape. It maps canonical material factors and standard
137
- texture bindings, preserves supported material extension data, and rejects
138
- missing primitive, material, or decoded texture references. It accepts triangle
139
- primitives; parsing, resource fetching, image decoding, and other topologies stay
140
- in the upstream conversion layer. Metallic-roughness and unlit workflows are
141
- supported; workflows without an existing Wavefront mapping fail explicitly.
37
+ The following browser module renders a single emissive triangle:
142
38
 
143
39
  ```js
144
- import { createCanonicalWavefrontMeshInputs } from "@plasius/gpu-renderer";
145
-
146
- const meshes = createCanonicalWavefrontMeshInputs({
147
- document: canonicalDocument,
148
- geometry: decodedPrimitives.map(({ primitiveId, positions, indices, normals, texcoords }) => ({
149
- primitiveId,
150
- positions,
151
- indices,
152
- normals,
153
- texcoords,
154
- })),
155
- // ReadonlyMap<canonical texture id, decoded RGBA sample>
156
- textures: decodedTextureSamples,
157
- });
158
- ```
159
-
160
- The canonical document uses types from `@plasius/gpu-model-core`. Geometry is
161
- already renderer-ready, normals may be omitted to retain the renderer's existing
162
- fallback, and texture samples are keyed by canonical texture ID. The consuming
163
- surface remains responsible for evaluating the remote
164
- `gpu.model.conversion.enabled` flag: when disabled or rolled back, keep the
165
- existing rendering path active.
166
-
167
- ## Adaptive Frame Hooks
168
-
169
- `@plasius/gpu-renderer` now exposes frame lifecycle hooks so the app can pass
170
- negotiated frame targets from `@plasius/gpu-performance` and opt into renderer
171
- frame sampling for `@plasius/gpu-debug`.
172
-
173
- ```js
174
- import { createGpuRenderer, createRendererDebugHooks } from "@plasius/gpu-renderer";
175
-
176
- const rendererDebugHooks = createRendererDebugHooks({
177
- debugSession,
178
- getTargetFrameTimeMs: () => governor.getSnapshot().targetFrameTimeMs,
179
- });
180
-
181
- const renderer = await createGpuRenderer({
182
- canvas: "#scene",
183
- frameIdFactory: ({ frame, xrActive }) => `scene.${xrActive ? "xr" : "flat"}.${frame}`,
184
- ...rendererDebugHooks,
185
- });
186
- ```
187
-
188
- ## Worker DAG Manifests
189
-
190
- The renderer also publishes worker-facing frame-stage manifests so
191
- `@plasius/gpu-performance` and `@plasius/gpu-worker` can reason about renderer
192
- work as a multi-root DAG instead of a flat queue.
193
-
194
- ```js
195
- import { getRendererWorkerManifest } from "@plasius/gpu-renderer";
196
-
197
- const realtimeManifest = getRendererWorkerManifest();
198
- const xrManifest = getRendererWorkerManifest("xr");
199
-
200
- console.log(realtimeManifest.jobs.map((job) => job.worker.jobType));
201
- console.log(xrManifest.jobs.find((job) => job.key === "lateLatch"));
202
- ```
203
-
204
- - `realtime` publishes `acquire`, `visibility`, `mainEncode`, `postProcess`,
205
- and `submit`.
206
- - `xr` publishes `acquire`, `visibility`, `lateLatch`, `mainEncode`, and
207
- `submit`.
208
- - Jobs include queue class, priority, dependencies, adaptive budget levels, and
209
- debug metadata such as allocation tags.
210
-
211
- ## Ray-Tracing-First Planning
212
-
213
- The renderer now publishes a stable-snapshot render plan for the premium
214
- ray-tracing-first frame model.
215
-
216
- ```js
217
- import { createRayTracingRenderPlan } from "@plasius/gpu-renderer";
218
-
219
- const plan = createRayTracingRenderPlan({
220
- snapshotId: "visual-snapshot-42",
221
- });
222
-
223
- console.log(plan.inputBoundary);
224
- console.log(plan.renderStages.map((stage) => stage.key));
225
- console.log(plan.representationBands);
226
- console.log(plan.wavefront.queueLayout.strategy);
227
- ```
228
-
229
- ## Authored Material Transport
230
-
231
- Wavefront materials support authored `KHR_materials_*` factors and extension
232
- textures, bounded nested media, Beer-Lambert attenuation, and reference
233
- transport helpers for reflected/transmitted branching and spectral dispersion.
234
- GPU rays carry up to four medium identifiers and enqueue one selected
235
- continuation per hit. Dielectric lobes are weighted by their selection
236
- probability; total internal reflection retains unit throughput. The standalone
237
- CPU reference helper can describe both branches, but the fixed GPU path never
238
- enqueues an untracked sibling into its tile-sized queue. Physically qualified
239
- nested media, rough dielectric transport and full spectral rendering remain
240
- separate work. See [the Amber correction](docs/amber-transport-correction.md).
241
-
242
- ```js
243
- import {
244
- createMediumStack,
245
- createTransportBranches,
246
- createSpectralSamples,
247
- } from "@plasius/gpu-renderer";
248
-
249
- const stack = createMediumStack([1, 4]);
250
- const branches = createTransportBranches({
251
- mediumStack: stack,
252
- mediumId: 7,
253
- transmission: 1,
254
- ior: 1.5,
255
- });
256
- const wavelengths = createSpectralSamples({ ior: 1.5, dispersion: 0.2 });
257
- ```
258
-
259
- The plan makes the stable visual snapshot boundary explicit, publishes the
260
- required RT-first stage ordering, and exposes representation-band plus
261
- acceleration-structure update policy metadata for downstream lighting and
262
- performance packages. It now also exposes the renderer-owned wavefront queue
263
- model, versioned ray/hit/surface/material/medium/accumulation contracts, and
264
- the termination policy for emissive/environment path completion.
265
-
266
- ## WebGPU Wavefront Compute Renderer
267
-
268
- The package also exposes an executable WebGPU wavefront renderer for active-ray
269
- debug validation scenes. It is compute-driven, tiled, and breadth-first by
270
- bounce depth, so queue buffers are bounded by tile size instead of presentation
271
- resolution. Renderer-owned GPU record sizes are part of the public compute
272
- limits so ray, hit, triangle, BVH, and accumulation buffers stay aligned with
273
- their WGSL layouts. Frame submission batching and dispatch diagnostics are kept
274
- in dedicated runtime helpers so performance-facing integrations can consume
275
- stable frame stats without inheriting the renderer's shader and pipeline
276
- assembly internals.
277
-
278
- ## Animated Scene Renderer
279
-
280
- `createAnimatedSceneRenderer` provides the renderer-owned v1 surface for the
281
- GPU animation adventure demo. It accepts scripted beats, route points, props,
282
- and a backward-compatible `lagged-follow` camera rig that now resolves through
283
- the shared `@plasius/gpu-camera` editor, spectator, third-person, and
284
- first-person rig primitives. The renderer exposes `start`, `resize`,
285
- `getSnapshot`, `setCamera`, `setCameraViewMode`, `applyCameraControl`, and
286
- `destroy` for host packages. Third-person distance is clamped by camera
287
- constraints, first-person resolves from the head anchor, and head-look is
288
- reported as transient post-animation intent instead of mutating clip data. It
289
- is implemented inside `gpu-renderer` and does not route animation playback
290
- through Three.js. When hosts provide
291
- `modelAsset` and `clipAssets`, the v1 canvas renderer parses the Peasant Girl
292
- GLB mesh, skin, inverse-bind matrices, and Mixamo-compatible clip channels,
293
- then CPU-skins the active clip and draws model-derived geometry into the
294
- adventure canvas. Snapshots expose `modelRenderable`, `fallbackProxyActive`,
295
- `skinnedVertexCount`, `skinnedJointCount`, `activeClipRenderable`,
296
- `cameraViewMode`, `cameraTransform`, `targetDistance`, `headLook`,
297
- `characterVisible`, `characterGroundY`, and `propGroundAnchors` so hosts can
298
- catch camera, scene grounding, and model-renderability regressions.
299
- When hosts provide beat `movementRequirement` fields and clip
300
- `movementProfile` metadata, character displacement is resolved per beat:
301
- travel/jump beats move between route anchors, while stationary action beats
302
- hold their current anchor unless the profile explicitly allows authored root
303
- translation. Snapshots expose movement validation diagnostics so hosts can
304
- detect mismatched animation motion before accepting playback.
305
-
306
- `createProfessionalAnimatedSceneRenderer` is the fail-closed WebGPU entrypoint
307
- for the professional Animation Adventure path. It requires a WebGPU canvas,
308
- skinned GLB character metadata with UVs, normals, diffuse and normal textures,
309
- and root-authored movement profiles for travel beats. It rejects the legacy
310
- 2D proxy path instead of silently falling back, renders through WebGPU, and
311
- exposes `renderMode: "webgpu-pbr"`, texture counts, normal-map readiness,
312
- root-motion policy, character position, camera position, and active clip
313
- diagnostics for host validation. Repeated travel beats accumulate root-motion
314
- distance from the clip duration and loop count, capped by the declared movement
315
- requirement and route segment. The current surface establishes the WebGPU
316
- lifecycle and validation boundary for the PBR animation path; shader-level
317
- textured character and environment drawing builds on this boundary.
318
-
319
- The production transport rollout remains gated by
320
- `renderer.transport.physicalEstimator`. With the flag enabled, deferred
321
- continuation vertices carry sanitized physical throughput segments instead of a
322
- heuristic material-response tint, while the older fallback path remains the
323
- rollback route during validation.
324
- Low-SPP physical lighting hardening is separately controlled by the boolean
325
- `renderer.transport.strictPhysicalLowSppLighting` flag, passed either as
326
- `strictPhysicalLowSppLighting: true` or through `featureFlags`. When enabled,
327
- the renderer disables terminal ambient rescue for max-depth/null-throughput
328
- termination, samples procedural sunlight as an explicit shadow-tested
329
- directional source, samples procedural sky over the visible hemisphere, and
330
- selects emissive triangles by area-weighted emission power with matching MIS
331
- PDFs. Use `denoise: false` when validating this strict path so remaining
332
- variance is measured in the transport rather than hidden by filtering.
333
- Strict validation can also enable the Product Studio transport experiment
334
- matrix through independent boolean feature flags. The flags are composable:
335
- none, one, several, or all may be requested at once. Flags that depend on strict
336
- physical transport report as requested but no-op effectively when
337
- `renderer.transport.strictPhysicalLowSppLighting` is disabled.
338
-
339
- - `renderer.transport.stableSampleRouting.enabled`
340
- - `renderer.transport.strictZeroOverflow.enabled`
341
- - `renderer.transport.deferLowSppRussianRoulette.enabled`
342
- - `renderer.transport.deterministicDirectLighting.enabled`
343
- - `renderer.transport.sourceStableDirectLighting.enabled`
344
- - `renderer.transport.deterministicLowSppIndirect.enabled`
345
- - `renderer.environment.productStudioImportance.enabled`
346
- - `renderer.diagnostics.productTransportTelemetry.enabled`
347
-
348
- Renderer config, frame stats, and snapshots expose both the structured
349
- `transportExperiments` state and the packed `transportExperimentFlags` bitfield
350
- so Product Studio diagnostics can record the exact active set for each render.
351
- `renderer.transport.sourceStableDirectLighting.enabled` is intended for
352
- denoise-off Product Studio validation: in strict physical mode it enables the
353
- deterministic direct-light path and removes direct-light sample dependence on
354
- adjacent pixel ids and frame index so low-SPP source routing is stable without
355
- adding ambient fill. Multi-bounce continuation rays that terminate on emissive
356
- geometry are MIS-weighted against that direct emissive estimator, so rare
357
- softbox hits remain physically valid without adding full unbalanced source
358
- radiance as isolated stippled pixels.
359
- `renderer.transport.deterministicLowSppIndirect.enabled` is also strict-mode
360
- only. The flag remains in the public experiment matrix so Product Studio can
361
- keep reporting requested and effective rollout state, but the strict shader path
362
- does not inject cached indirect radiance or suppress physical continuation. Any
363
- future low-SPP indirect stabilizer must be introduced as an auditable transport
364
- source with measured PDFs, visibility, and validation against high-SPP reference
365
- renders before it contributes radiance. Until then,
366
- `transportContributions.cachedIndirectLuminance` should remain zero and
367
- multi-bounce energy should come from direct explicit lighting, true terminal
368
- emissive/environment hits, or stochastic BSDF continuation.
369
- Set `presentationOutput: "linear"` for linear presented validation captures, or
370
- omit it to keep the default tone-mapped presentation.
371
-
372
- ```js
373
- import {
374
- createWavefrontPathTracingComputeRenderer,
375
- } from "@plasius/gpu-renderer";
40
+ import { createWavefrontPathTracingComputeRenderer } from "@plasius/gpu-renderer";
376
41
 
377
42
  const renderer = await createWavefrontPathTracingComputeRenderer({
378
- canvas: document.querySelector("#product-render"),
379
- width: 1280,
380
- height: 720,
381
- maxDepth: 6,
382
- samplesPerPixel: 8,
383
- displayQuality: true,
384
- meshes: [
385
- {
386
- id: 1,
387
- positions: [-1, -1, 0, 1, -1, 0, 0, 1, 0],
388
- indices: [0, 1, 2],
389
- normals: [0, 0, 1, 0, 0, 1, 0, 0, 1],
390
- emission: [8, 7, 5, 1],
391
- },
392
- ],
43
+ canvas: "#scene",
44
+ width: 640,
45
+ height: 360,
46
+ maxDepth: 2,
47
+ samplesPerPixel: 1,
48
+ meshes: [{
49
+ id: 1,
50
+ positions: [-1, -1, 0, 1, -1, 0, 0, 1, 0],
51
+ indices: [0, 1, 2],
52
+ normals: [0, 0, 1, 0, 0, 1, 0, 0, 1],
53
+ emission: [8, 7, 5, 1],
54
+ }],
393
55
  });
394
56
 
395
- renderer.renderOnce();
396
- ```
397
-
398
- Existing consumers that still call `renderFrame(...)` or
399
- `renderWavefrontPathTracingComputeFrame(...)` remain supported as compatibility
400
- wrappers around the canonical mesh renderer.
401
-
402
- Analytic scene objects remain available for debug fixtures:
403
-
404
- ```js
405
- const debugRenderer = await createWavefrontPathTracingComputeRenderer({
406
- canvas: document.querySelector("#debug-render"),
407
- width: 1280,
408
- height: 720,
409
- maxDepth: 6,
410
- sceneObjects: [
411
- {
412
- type: "sphere",
413
- center: [0, 1.8, -0.5],
414
- radius: 0.35,
415
- emission: [8, 7, 5, 1],
416
- materialKind: "emissive",
417
- },
418
- ],
419
- });
57
+ await renderer.renderFrame();
420
58
 
421
- debugRenderer.renderOnce();
59
+ // Call this from your route or component cleanup.
60
+ export function dispose() {
61
+ renderer.destroy();
62
+ }
422
63
  ```
423
64
 
424
- Scene objects currently support analytic `sphere` and axis-aligned `box`
425
- records with colour, emission, roughness, metallic, opacity, IOR, clearcoat,
426
- sheen colour, specular colour, and transmission fields.
427
- These records are debug fixtures only. Product Studio visual rendering requires
428
- the mesh BVH path described in
429
- `docs/adrs/adr-0007-triangle-mesh-wavefront-path-tracing.md`. This is a
430
- project-wide display-quality baseline for path-traced rendering, not a
431
- Product-Studio-only requirement.
432
-
433
- Mesh inputs are normalized into triangle records, packed into GPU buffers, and
434
- uploaded as source buffers for tracing. Vertex normals are preserved for smooth
435
- shading; when normals are absent the triangle geometric normal is used. The
436
- display-quality path now defaults to `accelerationBuildMode: "cpu-upload"` so
437
- CPU-built BVH nodes and triangle records are uploaded once and then reused by
438
- the GPU tracing passes. Set `accelerationBuildMode: "gpu"` only when you
439
- explicitly want the experimental GPU-side BVH assembly path for validation or
440
- development. The `createWavefrontMeshAcceleration(...)` helper is therefore no
441
- longer debug-only; it is the stable display-quality acceleration builder,
442
- whereas GPU BVH construction remains available behind the explicit mode switch.
443
- CPU-upload records still preserve the raw material factors plus atlas rects and
444
- texture settings expected by the GPU hit-time samplers; they are not meant to
445
- replace exact UV-driven sampling with CPU-baked triangle averages.
446
- The GPU BVH path still uses Morton-style centroid keys to sort leaf references
447
- before sorted leaves and level-concurrent internal nodes are materialized.
448
- When mesh inputs also carry UVs plus decoded base-colour,
449
- metallic-roughness, normal, occlusion, or emissive maps, the display-quality
450
- path now packs them into GPU texture atlases and samples them at the resolved
451
- hit UV inside the wavefront trace pass. Generic glTF-style material factors
452
- such as clearcoat, sheen colour, specular colour, transmission, and IOR are
453
- also preserved through the GPU records so demo validation does not need
454
- model-name overrides. CPU-side texture work is limited to load-time decode and
455
- atlas packing; per-hit shading stays on the GPU. Direct and terminal glossy
456
- response now also samples reflection-aligned environment radiance so leather,
457
- chrome, and other polished authored materials can read from the active
458
- environment map or procedural sky instead of relying mostly on a sun-direction
459
- proxy.
460
- Authored participating-media inputs can also enter through the same mesh path.
461
- A mesh may provide an explicit `medium` block or a glTF-style `material.volume`
462
- block with `thickness`, `attenuationColor`, and `attenuationDistance`; the
463
- renderer derives the medium-table entry, carries the active medium id on the
464
- GPU ray, and applies Beer-Lambert transmittance to travelled segments on the
465
- GPU. Thickness is now preserved in material packing for later shell-volume
466
- work, but the current renderer still tracks only one active medium id per ray
467
- and does not yet implement nested media, spectral dispersion, or a branching
468
- reflection/refraction tree. Invalid medium ids fall back to the current ray
469
- medium, and entering a second medium while one is already active preserves the
470
- existing medium id until dedicated stack support is implemented.
471
-
472
- `samplesPerPixel` controls how many GPU primary-ray samples are accumulated per
473
- screen pixel within a single render. This multiplies dispatch work but does not
474
- increase the tile queue memory footprint, so 720p/1080p/4K targets remain
475
- bounded by `tileSize`. `maxDepth` is bounded at 32 for offline/reference
476
- renders, allowing 20-bounce quality checks while keeping path-vertex memory
477
- explicit and predictable. When `denoise` is enabled the renderer writes raw
478
- linear radiance to an `rgba16float` texture first, then runs a two-stage
479
- full-frame GPU denoise through an `rgba16float` scratch texture before final
480
- tone mapping into the presented `rgba8unorm` output. Filtering in linear
481
- radiance space lets the denoise pass cross tile boundaries without compressing
482
- energy/detail before the final resolve. High-SPP wavefront sampling now routes
483
- camera jitter, BSDF continuation, emissive-light selection, and environment
484
- selection through named sample dimensions in
485
- `src/wavefront-sampling-dimensions.js`, using low-discrepancy 2D pairs where
486
- they improve convergence without adding CPU-side sample tables. The companion
487
- `src/wavefront-denoise-validation.js` acceptance helper keeps denoise-off
488
- validation explicit with structural-artifact, invalid-sample, baseline-noise,
489
- and sheen/chrome/wood detail-retention thresholds so the
490
- `renderer.denoise.highSppIndependence` rollout can stay reviewable and
491
- rollbackable. The renderer also stores compact
492
- emissive-triangle metadata in the existing BVH buffer tail and uses it to guide
493
- diffuse continuation rays toward finite mesh light geometry. This is not a
494
- separate shadow/direct-light pass: the active ray still has to hit emissive
495
- geometry or miss into the environment before radiance is committed. Guided
496
- emissive hits carry a bounded estimator weight so finite light guidance does not
497
- over-expose low-sample renders before full material PDFs/MIS are implemented.
498
- By default, `deferredPathResolve` records
499
- per-bounce material responses in a tile-bounded path buffer and records the
500
- terminal emissive/HDRI/environment source in the final path slot. The output
501
- pass then resolves that recorded path backward and adds the weighted sample to
502
- the pixel accumulation, so unresolved continuation light is still deferred until
503
- a terminal source is known. Surface resolution may still add a small
504
- shadow-tested direct-light term immediately when it has an explicit source and
505
- visibility result, which keeps true occlusion shadows possible without falling
506
- back to broad per-bounce ambient fill. Set `deferredPathResolve: false` only
507
- for legacy forward-accumulation comparison.
508
- When an `environmentMap` is provided, the wavefront trace shader samples it as
509
- an equirectangular radiance source for environment misses and uses the same
510
- mapped radiance for terminal residuals before falling back to static ambient.
511
- The procedural horizon/zenith/sun model remains the fallback for callers that
512
- have not supplied an HDRI/radiance texture. `environmentLighting.sunlitBaseline`
513
- adds a time-of-day daylight floor to terminal and direct environment estimates,
514
- so bright presets retain colour at the last collision without returning to a
515
- whitewashed global ambient term. Extremely dark recorded bounce responses are
516
- also remapped to a small scene-brightness-driven luminance floor so bright
517
- low-sample scenes do not produce isolated black speckles when a valid terminal
518
- source was already found. Awaited `renderFrame({ readStats: true })` results now
519
- keep linear accumulation unclamped, sanitize only invalid or half-float-
520
- overflow samples before presentation, and expose
521
- `terminalRadiance.totalLuminance`,
522
- `terminalRadiance.ambientResidualLuminance`, and
523
- `terminalRadiance.ambientResidualShare` so validation harnesses can track how
524
- much of the final resolved image came from terminal ambient residuals.
525
- The same stats also expose `radianceDiagnostics.invalidSamples` and
526
- `radianceDiagnostics.legacyClampEquivalentSamples`, so hardening scenes can
527
- measure NaN/Inf cleanup and legacy-4.0-equivalent fireflies without
528
- re-introducing a hidden estimator clamp.
529
- When `strictPhysicalLowSppLighting` is enabled, termination stats also separate
530
- `absorptionNull`, `russianRoulette`, and `strictMaxDepth` so validation can
531
- distinguish physically explainable dark samples from legacy ambient fallback.
532
- For static mesh scenes, the GPU acceleration build is submitted once and then
533
- reused by subsequent frames. Per-frame tracing writes one dynamic uniform slot
534
- per tile/sample or post-process pass and batches tile tracing, tile output,
535
- optional denoise, and presentation into bounded command submissions controlled
536
- by `maxFramePassesPerSubmission` to keep 4K/high-spp command buffers from
537
- becoming oversized. `updateCamera(...)` can update the per-frame camera uniforms
538
- without rebuilding scene buffers. `renderFrame(...)` also accepts an optional
539
- `frameTimeBudgetMs` plus `minimumSamplesPerPixel`: when present, configured
540
- `samplesPerPixel` becomes a ceiling instead of a hard requirement, the renderer
541
- guarantees at least the minimum full-screen pass, and frame stats report both
542
- configured `samplesPerPixel` and actual `renderedSamplesPerPixel` so realtime
543
- callers can budget motion frames without overstating delivered quality.
544
- Before requesting a WebGPU device, the renderer derives the largest scene
545
- storage binding from the normalized scene, mesh, and BVH records. It requests
546
- the smallest sufficient `maxStorageBufferBindingSize` (and `maxBufferSize` only
547
- when the WebGPU default is also exceeded), preserves stricter caller limits,
548
- and rejects unsupported adapters before device creation. Default-sized scenes
549
- do not elevate either size limit. `config.memory` and snapshot memory telemetry
550
- describe the actual persistent GPU buffer descriptors: placeholder records,
551
- combined BVH/emissive storage, mesh source buffers, aligned frame/build uniform
552
- buffers, counters, and dispatch arguments are each counted exactly once;
553
- `materialTableBytes` is zero because material data is packed into mesh and
554
- triangle records rather than a standalone GPU buffer.
555
- For consumers that want to hand wavefront SPP adaptation to
556
- `@plasius/gpu-performance`, `createWavefrontAdaptiveSamplingLevels(...)` exposes
557
- a bounded low-to-high ladder of per-frame `samplesPerPixel`,
558
- `frameTimeBudgetMs`, and `minimumSamplesPerPixel` configs that stay aligned
559
- with the renderer's supported adaptive-sampling surface. Frame stats and
560
- snapshots expose
561
- `gpuParallelism` diagnostics with adapter compute limits, configured workgroup
562
- size, direct compute dispatches, known workgroups/invocations, indirect dispatch
563
- counts, and upper-bound indirect work estimates. WebGPU does not expose physical
564
- GPU core counts, so `physicalCoreCount` remains `null`; use
565
- `exposesMultiWorkgroupParallelism`, `largestDirectWorkgroupsPerDispatch`, and
566
- `largestEstimatedIndirectWorkgroupsPerDispatch` to confirm the renderer is
567
- issuing multi-workgroup GPU work. Awaited frame results also expose a
568
- `transportGuardrails` summary with jobs/frame, jobs/s, jobs/submission, command
569
- submissions, queue-overflow count, radiance diagnostics, total tracked memory
570
- bytes, and device-loss status so validation harnesses can gate transport work
571
- without scraping ad hoc metrics from multiple fields. Treat any sustained >10%
572
- drop in jobs/submission or jobs/s versus the approved baseline as a
573
- release-validation failure unless a linked ticket explicitly approves the
574
- regression; `submission-batching` warns when the renderer falls back to roughly
575
- one GPU job per submission despite a higher pass ceiling.
576
-
577
- Awaited fixed-SPP renders can opt into exact ray and timing evidence:
65
+ Supply decoded triangle geometry and material/texture data through `meshes`.
66
+ The renderer does not fetch glTF files: use a loader such as
67
+ [`@plasius/gpu-shared`](https://www.npmjs.com/package/@plasius/gpu-shared), or adapt
68
+ canonical decoded assets with `createCanonicalWavefrontMeshInputs`.
69
+ For an interactive model viewer with loading, integrity checks, camera gestures
70
+ and quality controls, use the shared package's `model-inspection` entry.
578
71
 
579
- ```js
580
- const frame = await renderer.renderFrame({
581
- readStats: true,
582
- readOutputProbe: false,
583
- });
584
-
585
- console.log({
586
- primaryRays: frame.primaryRays,
587
- secondaryRays: frame.secondaryRays,
588
- totalPathSegments: frame.totalPathSegments,
589
- bounceHistogram: frame.rayCounts.bounceHistogram,
590
- gpuTimeMs: frame.timings.totalGpuTimeMs,
591
- renderJobTimeMs: frame.timings.totalRenderJobTimeMs,
592
- timingSource: frame.timings.source,
593
- });
594
- ```
72
+ ## Choose a rendering API
595
73
 
596
- `primaryRays` is the exact number of camera samples. `secondaryRays` is the
597
- sum of active continuation records intersected after bounce zero, including
598
- bounded reflection/transmission branches, and `totalPathSegments` is their sum.
599
- The renderer copies the existing active-queue counter once per bounce into a
600
- lazily allocated telemetry buffer; it does not add diagnostic shader atomics or
601
- change WGSL transport. `rayCounts.status` distinguishes available,
602
- unavailable, failed, and not-requested evidence. A mismatch between scheduled
603
- and observed primary rays fails the `ray-count-telemetry` transport guardrail.
604
- Ray-count reduction uses constant call-stack space, including the supported
605
- 4K/eight-bounce/128-SPP stress envelope where hundreds of thousands of
606
- tile/sample/depth records collapse into a small per-bounce histogram.
607
-
608
- When the adapter exposes `timestamp-query`, the device requests it
609
- opportunistically and two timestamps measure the GPU span from the first
610
- primary pass through presentation. Set `gpuTimestamps: false` at renderer
611
- creation to prevent that optional feature request. Where timestamp queries are
612
- not exposed or fail, `timings` reports `source: "queue-completion"`, leaves
613
- `totalGpuTimeMs` null, and retains the awaited
614
- `totalRenderJobTimeMs`. Classification, compaction, and sampling sub-pass times
615
- remain null until the corresponding adaptive passes exist. Without
616
- `readStats: true`, and for `renderOnce()`, the fixed dispatcher creates no
617
- telemetry buffers, query sets, readbacks, or timestamped pass descriptors.
618
-
619
- After each
620
- primary-ray or compaction pass, the GPU writes the active-ray workgroup count
621
- into the counter buffer and the encoder copies it into an indirect-dispatch
622
- argument buffer. Intersection and surface-resolution passes therefore scale
623
- with active continuation rays instead of the maximum tile capacity, while still
624
- avoiding CPU readback between bounces. WebGPU
625
- still preserves ordering between dependent bounce passes, but the renderer
626
- keeps CPU queue submissions bounded rather than forcing one submission per
627
- tile/sample. Awaited higher-SPP submission slicing remains tile-major because
628
- the accumulation buffer is tile-local; changing that order to sample-major
629
- would mix samples across tiles and corrupt the resolved image.
630
- Environment-light portals can additionally guide and gate sky/HDRI contribution
631
- through rectangular openings such as windows. `environmentPortalMode: "guide"`
632
- biases diffuse continuation rays toward configured openings, while
633
- `"guide-and-gate"` requires an environment miss to pass through a portal before
634
- it receives sky radiance; misses outside a portal fall back to the ambient
635
- residual. This keeps interior rooms from treating the whole sky as visible from
636
- every bounce.
637
- Texture sampling, dynamic TLAS updates, higher-grade LBVH/SAH construction,
638
- runtime execution behind the `@plasius/gpu-worker` lock-free queue, and broader
639
- material lookup remain follow-up work.
640
-
641
- ## XR integration
74
+ | API | Purpose |
75
+ | --- | --- |
76
+ | `createWavefrontPathTracingComputeRenderer` | Tiled, mesh-BVH path tracing with material transport, environment lighting and optional progressive accumulation |
77
+ | `createNativeSceneRenderer` | Native raster triangles, authored surface maps, shadows and optional animated water |
78
+ | `createGpuRenderer` | Device/canvas setup, clear pass, frame hooks and XR binding for a host-owned pipeline |
79
+ | `createProfessionalAnimatedSceneRenderer` | The validated WebGPU animation scene path with skinned assets and root-motion requirements |
80
+ | `createAnimatedSceneRenderer` | The existing canvas animation-adventure surface |
81
+ | `createRayTracingRenderPlan` | Rendering plan metadata; it does not draw a scene |
82
+
83
+ All factories are exported from `@plasius/gpu-renderer`. Keep the import and GPU
84
+ initialization inside the route that needs them. The host owns loading/error UI,
85
+ visibility policy, accessible controls, and disposal when leaving the view.
86
+
87
+ ## Progressive rendering
88
+
89
+ Set `progressive: true` when creating the wavefront renderer. Its creation-time
90
+ `samplesPerPixel` sets batch capacity; each `renderFrame({ samplesPerPixel })`
91
+ may submit a smaller batch. Await batches serially and stop scheduling when your
92
+ sample target is reached or the view is hidden. `renderFrame()` waits for GPU
93
+ completion by default.
94
+
95
+ Camera and scene changes reset accumulated radiance. `updateCamera()` preserves
96
+ the loaded meshes, materials and acceleration resources; do not call it between
97
+ refinement passes if the view has not changed. `resetAccumulation()` discards
98
+ history explicitly. Denoising runs after accumulation.
99
+
100
+ `getSnapshot().accumulatedSamplesPerPixel` counts **submitted** samples. If you
101
+ use synchronous `renderOnce()`, await `renderer.device.queue.onSubmittedWorkDone()`
102
+ before labelling that work complete. Public sampling is fixed per frame;
103
+ experimental per-pixel adaptive metadata does not enable adaptive rendering.
104
+ See the [wavefront guide](https://github.com/Plasius-LTD/gpu-renderer/blob/main/docs/guides/wavefront.md)
105
+ for memory, timing, transport flags and progressive behavior.
106
+
107
+ ## Frame lifecycle without a scene
108
+
109
+ Use `createGpuRenderer` when your application owns the rendering pipeline. This
110
+ example clears the canvas and runs the frame lifecycle; it does not load or draw
111
+ 3D models:
642
112
 
643
113
  ```js
644
- import { createXrManager } from "@plasius/gpu-xr";
645
114
  import { createGpuRenderer } from "@plasius/gpu-renderer";
646
115
 
647
- const renderer = await createGpuRenderer({ canvas: "#scene" });
648
- const xr = createXrManager();
649
-
650
- renderer.bindXrManager(xr, {
651
- onSessionStart: () => console.log("XR active"),
652
- onSessionEnd: () => console.log("XR inactive"),
116
+ const renderer = await createGpuRenderer({
117
+ canvas: "#scene",
118
+ clearColor: "#102035",
653
119
  });
120
+ renderer.resize(640, 360);
121
+ renderer.start();
122
+
123
+ export function dispose() {
124
+ renderer.destroy();
125
+ }
654
126
  ```
655
127
 
656
- ## API
657
-
658
- - `supportsWebGpu(options)`
659
- - `createGpuRenderer(options)`
660
- - `createRendererDebugHooks(options)`
661
- - `getRendererWorkerProfile(name?)`
662
- - `getRendererWorkerManifest(name?)`
663
- - `createRayTracingRenderPlan(options)`
664
- - `createWavefrontPathTracingComputeRenderer(options)`
665
- - `createWavefrontPathTracingComputeConfig(options)`
666
- - `createWavefrontPathTracingComputeShaderSource(options?)`
667
- - `renderWavefrontPathTracingComputeFrame(options)`
668
- - `createWavefrontReferenceRay(config, options?)`
669
- - `intersectWavefrontReferenceTriangle(ray, triangle, options?)`
670
- - `traceWavefrontReferenceTriangles(config, ray, triangles, options?)`
671
- - `normalizeWavefrontMesh(input)`
672
- - `createWavefrontGpuMeshSource(meshes)`
673
- - `createWavefrontBvhSortStages(itemCount)`
674
- - `createWavefrontBvhBuildLevels(triangleCount)`
675
- - `createWavefrontMeshAcceleration(meshes)`
676
- - `normalizeWavefrontSceneObject(input)`
677
- - `packWavefrontSceneObjects(sceneObjects, capacity?)`
678
- - `packWavefrontTriangles(triangles, capacity?)`
679
- - `packWavefrontBvhNodes(nodes, capacity?)`
680
- - `rendererWavefrontComputeMode`
681
- - `rendererWavefrontComputeWorkgroupSize`
682
- - `rendererWavefrontComputeStatsStride`
683
- - `bindRendererToXrManager(renderer, xrManager, options)`
684
- - `defaultRendererClearColor`
685
- - `rendererDebugOwner`
686
- - `rendererWorkerQueueClass`
687
- - `defaultRendererWorkerProfile`
688
- - `rendererWorkerProfiles`
689
- - `rendererWorkerProfileNames`
690
- - `rendererWorkerManifests`
691
-
692
- The reference helpers mirror the renderer WGSL camera and triangle-hit math in
693
- deterministic JavaScript so tests and downstream tooling can validate primary
694
- ray generation, barycentrics, nearest-hit selection, and environment misses
695
- without standing up a WebGPU device.
696
-
697
- ## Demo
698
-
699
- Run the demo server from the repo root:
128
+ ## API and integration guides
700
129
 
701
- ```sh
702
- cd gpu-renderer
703
- npm run demo
704
- ```
130
+ - [Wavefront path tracing, materials and progressive refinement](https://github.com/Plasius-LTD/gpu-renderer/blob/main/docs/guides/wavefront.md).
131
+ - [Native surfaces, texture maps and clearcoat](https://github.com/Plasius-LTD/gpu-renderer/blob/main/docs/guides/native-surfaces.md).
132
+ - [Canonical ingestion, frame hooks, worker manifests, XR and diagnostics](https://github.com/Plasius-LTD/gpu-renderer/blob/main/docs/guides/integration.md). Snippets in this reference use host-provided decoded assets and services.
133
+ - [Animation scene contracts](https://github.com/Plasius-LTD/gpu-renderer/blob/main/docs/guides/animation.md).
134
+ - [Complete public TypeScript declarations](https://github.com/Plasius-LTD/gpu-renderer/blob/main/src/index.d.ts).
135
+ - [Crate material-study implementation notes](https://github.com/Plasius-LTD/gpu-renderer/blob/main/docs/guides/material-study-internals.md). Fixture helpers are internal; mount the study through `@plasius/gpu-shared/material-study`.
705
136
 
706
- Then open `http://localhost:8000/gpu-renderer/demo/`.
137
+ Experimental transport switches use the `featureFlags` options described in the
138
+ wavefront guide. The package consumes supplied decisions; it does not connect to
139
+ an account or remote flag service. Plasius production consumers also apply their
140
+ route capabilities and rollout policy before mounting a scene.
707
141
 
708
- The demo now mounts the mesh BVH WebGPU wavefront renderer directly and passes a
709
- `@plasius/gpu-lighting` environment preset into the render. It reports the
710
- active wavefront depth, tile count, triangle/BVH counts, lighting preset, probe
711
- luminance, and hot buffer memory so it is clear whether the renderer is tracing
712
- mesh paths rather than only showing planning metadata.
142
+ ## Development
713
143
 
714
- ## Development Checks
144
+ From a checkout, using the repository's Node.js version:
715
145
 
716
146
  ```sh
147
+ npm ci --legacy-peer-deps
717
148
  npm run lint
718
149
  npm run typecheck
719
150
  npm run test:coverage
720
151
  npm run build
721
152
  npm run pack:check
153
+ npm run zero-three
722
154
  ```
723
155
 
724
- ## Files
725
-
726
- - `src/index.js`: public package facade for renderer runtime, render-plan, and
727
- wavefront exports.
728
- - `src/renderer-*.js`: framework-agnostic renderer constants, validation,
729
- worker manifests, wavefront render plans, and WebGPU runtime/XR binding
730
- helpers.
731
- - `src/wavefront-compute.js`: canonical WebGPU mesh BVH wavefront renderer
732
- lifecycle, live state, and public renderer instance methods.
733
- - `src/wavefront-acceleration-builder.js`, `src/wavefront-frame-encoder.js`,
734
- `src/wavefront-frame-dispatcher.js`, and `src/wavefront-frame-stats.js`:
735
- purpose-specific acceleration build, pass encoding, tile/sample dispatch, and
736
- frame-stat policy helpers.
737
- - `src/wavefront-bind-groups.js`, `src/wavefront-pipelines.js`,
738
- `src/wavefront-gpu-synchronization.js`, and `src/wavefront-readbacks.js`:
739
- WebGPU bind-group construction, pipeline construction, queue synchronization,
740
- and readback/probe helpers.
741
- - `src/wavefront-config.js`: wavefront camera, environment, portal, memory, and
742
- scene-source configuration.
743
- - `src/wavefront-scene-data.js`: compatibility facade for wavefront scene data
744
- helpers.
745
- - `src/wavefront-materials.js`, `src/wavefront-scene-normalizers.js`, and
746
- `src/wavefront-mesh-sources.js`: material/medium normalization, scene/mesh
747
- normalization, texture-atlas creation, BVH source generation, and GPU mesh
748
- source packing.
749
- - `src/wavefront-shaders.js` and `src/wavefront-shader-*.js`: WGSL assembly
750
- and purpose-specific shader source sections for layout, materials, lighting,
751
- BVH/intersection, and render kernels.
752
- - `src/wavefront-gpu-resources.js`, `src/wavefront-packers.js`, and
753
- `src/wavefront-runtime-support.js`: GPU resources, binary record packers, and
754
- WebGPU runtime/pipeline support.
755
- - `src/wavefront-reference.js` and `src/wavefront-sampling.js`: deterministic
756
- reference transport and sampling helpers used by validation tests.
757
- - `src/index.d.ts`: public API typings.
758
- - `scripts/check-source-syntax.cjs`: syntax-checks every JavaScript source file
759
- included under `src/`.
760
- - `tests/package.test.js`: unit tests for renderer lifecycle behavior.
761
- - `docs/design/worker-manifest-integration.md`: renderer frame-stage DAG model.
762
- - `docs/adrs/*`: architecture decisions for renderer runtime design.
763
- - `docs/tdrs/*`: technical direction for frame hook integration.
156
+ `npm run demo` requires Python 3 and serves the parent workspace. With the checkout
157
+ named `gpu-renderer`, visit `http://localhost:8000/gpu-renderer/demo/`.
158
+ See the [development guide](https://github.com/Plasius-LTD/gpu-renderer/blob/main/docs/guides/development.md)
159
+ for sibling-package requirements and source layout, and
160
+ [CONTRIBUTING.md](https://github.com/Plasius-LTD/gpu-renderer/blob/main/CONTRIBUTING.md).
161
+
162
+ Three.js and dependency paths reaching it are prohibited across runtime,
163
+ development tooling and published artifacts. `npm run zero-three:source` checks
164
+ source admission before installation; `npm run zero-three` validates the installed
165
+ and built package evidence.
764
166
 
765
167
  <!-- BEGIN PLASIUS RELEASE INTEGRITY -->
766
168
  ## Release integrity
@@ -790,204 +192,3 @@ there is no npm write-token fallback. CD remains disabled until the npm trusted
790
192
  publisher binding and protected-branch-only production environment are
791
193
  independently verified.
792
194
  <!-- END PLASIUS RELEASE INTEGRITY -->
793
-
794
- ### Authored materials in native surfaces
795
-
796
- `createNativeSceneRenderer({ canvas, materials })` accepts up to 16 opaque
797
- materials containing decoded RGBA8 `baseColor`, `normal`, and/or `orm` maps.
798
- This matches images returned by the `@plasius/gpu-shared` glTF loader. Colour maps
799
- are sRGB; normals and ORM (occlusion/roughness/metalness) are linear. Optional
800
- `normalScale` defaults to 1. Maps must be power-of-two, up to 2048 pixels per axis,
801
- with a 64 MiB combined decoded-source budget. Textures are uploaded once and mip
802
- filtered; the renderer owns GPU disposal, while callers own image loading.
803
-
804
- For textured frames, supply `texcoords` (two floats per vertex) and `surfaces`
805
- containing `{ firstVertex, vertexCount, materialIndex }`. Ranges must cover the
806
- whole triangle list in order, refer to the initialization-time materials, and
807
- contain whole triangles. At most 256 ranges are accepted. Linear vertex colour
808
- and roughness/metalness factors multiply the maps. Omit both new frame fields to
809
- retain the existing procedural/untextured path. Main and reflection passes use
810
- the same authored material; alpha/transmission/skinning are not added here.
811
-
812
- See [material design](docs/design/native-textured-surfaces.md) and
813
- [ADR-0031](docs/adrs/adr-0031-native-authored-materials.md). Rollout inherits
814
- `gpu-demo.scene-fidelity.enabled`; access remains governed by the site's capability.
815
-
816
- For varnished wood, set optional `clearcoat` and `clearcoatRoughness` (both 0–1).
817
- The coating reflects neutral light using its own smooth geometric normal above
818
- the coloured, normal-mapped wood; Fresnel layering attenuates the underlying
819
- material instead of simply adding shine. Strength defaults to zero, preserving
820
- uncoated callers. Coating roughness defaults to zero and is independent of the
821
- base roughness map. Optional `clearcoatMap` uses linear RGBA8: red multiplies
822
- `clearcoat` coverage and green multiplies `clearcoatRoughness`; blue and alpha
823
- are ignored. Omission uses white, preserving the scalar-only finish. Use a
824
- nonzero roughness factor (for example 1) to author roughness directly in green.
825
- The map shares the surface UVs, mip filtering, memory budget and GPU lifetime of
826
- other maps. Clearcoat normal maps remain unsupported.
827
-
828
- For mixed objects, give uncoated parts their own surface ranges with
829
- `clearcoat: 0`. This keeps rope and hardware uncoated even when they share the
830
- wood's atlas. The source example [native-crate-finish.js](https://github.com/Plasius-LTD/gpu-shared/tree/main/src/material-study-helpers/native-crate-finish.js)
831
- provides reviewed crate ranges and a deterministic wear preset combining existing
832
- roughness/scuff detail with broad UV patches. Its 0–1 wear control changes coat
833
- coverage and roughness without modifying the underlying colour or source asset.
834
- See [spatial coating decision](docs/adrs/adr-0032-spatial-native-clearcoat.md) and
835
- the [fixture/provenance record](docs/design/cc0-demo-asset-review.md). The example
836
- helper is source-only; the published API is the generic material map.
837
-
838
- For subtle rope fraying, the source-demo helper
839
- [native-rope-fibres.js](https://github.com/Plasius-LTD/gpu-shared/tree/main/src/material-study-helpers/native-rope-fibres.js) exports `createRopeFibres`.
840
- Pass the expanded native `vertices`, `texcoords` and explicit rope-only
841
- `{ firstVertex, vertexCount }` ranges. It returns separate triangles/UVs and a
842
- `fibreCount`; append them once with their own draw range. Generate in metre-scale
843
- world coordinates after transforms. Sampling is deterministic and area-weighted,
844
- with short tapered curves in small aligned tufts. `density` controls roots per
845
- square metre; `maxFibres` caps them at 4,000 (default 1,600). `length` and `radius`
846
- are in metres. The 21 triangles per strand use ordinary native multisampling,
847
- depth and lighting; no animated fur or dedicated hair scattering is claimed.
848
-
849
- Optional `fray` (0–1, default 0.08) promotes exactly
850
- `round(fibreCount * fray)` strands to longer curled loose ends, reported as
851
- `looseEndCount`. Selection is seeded and nested: raising the level keeps earlier
852
- loose ends, roots and UVs in place. At 0, short fuzz remains. This is a visual
853
- proportion of generated fibres, not lost rope strength.
854
-
855
- Use `createRopeFibreMaterial(ropeMaterial)` for the added range: it keeps the
856
- rope's colour map and zero clearcoat while omitting the core's baked occlusion
857
- and normal creases from exposed fibres. Vertex roughness is 1 and metallic is 0.
858
- The review fixture offers fibre on/off and both rope-detail views. See
859
- [ADR-0033](docs/adrs/adr-0033-demo-rope-fibres.md). These helpers remain source-only
860
- and do not add a renderer API, dependency or network request.
861
-
862
- ### Material ageing review wrapper
863
-
864
- [material-ageing.js](https://github.com/Plasius-LTD/gpu-shared/tree/main/src/material-study-helpers/material-ageing.js) validates and freezes a serializable
865
- version-1 profile. [native-crate-ageing.js](https://github.com/Plasius-LTD/gpu-shared/tree/main/src/material-study-helpers/native-crate-ageing.js) applies it
866
- through the existing coating/fibre helpers:
867
-
868
- ```js
869
- import { createCrateAgeing } from '@plasius/gpu-shared/material-study';
870
-
871
- const ageing = createCrateAgeing({ ropeFray: 0.30, varnishWear: 0.90, seed: 7349 });
872
- const { wood, rope, metal, fibres: fibreMaterial } = ageing.createMaterials(sourceMaps);
873
- const fibreGeometry = ageing.createFibres({ vertices, texcoords, ranges: ropeRanges, heightOffset: -minY });
874
- // Append fibreGeometry with fibreMaterial using the existing native draw contract.
875
- ```
876
-
877
- Those values are also the review defaults. `varnishWear` is independent visual
878
- intensity, not coating area removed or elapsed years. Broad scuff patches expose
879
- wood while sheltered areas retain coating at intermediate wear. Above 70%, the
880
- remaining islands smoothly erode; at 100%, every coverage texel and the scalar
881
- coat strength are zero, matching varnish disabled. The original wood maps remain
882
- unchanged, including their ordinary substrate light response. `seed` controls fibre placement and
883
- loose-end selection; varnish uses its existing stable UV pattern. The crate adapter
884
- uses a 4.5 mm fibre length scale and 0.16 mm root radius, with a 4,000-fibre budget;
885
- loose ends are 1.3–2 times the length scale and taper to a point. Source images and
886
- geometry remain unchanged; rope, added fibres and metal never acquire clearcoat.
887
-
888
- Generate/copy geometry only when the fray channel changes; rebuild finish maps
889
- only when coating settings change. Camera movement reuses both. The caller still
890
- owns verified asset ranges, GPU resources and disposal. The local review includes
891
- fresh/aged presets, fibre/coat toggles and both handle-detail views.
892
-
893
- This is a tested source-demo wrapper, not a newly published package or a complete
894
- damage simulator. [ADR-0034](docs/adrs/adr-0034-material-ageing-profile.md) records
895
- the proposed `@plasius/material-ageing` boundary after a second asset establishes
896
- the shared contract. Production adoption retains `gpu-demo.scene-fidelity.enabled`
897
- and the existing GPU route capability; disabling the feature selects the existing
898
- site fallback. No package publication or site deployment is implied by this study.
899
-
900
-
901
- For structured nap, `createRopeFibres` accepts optional `guides`: at most eight
902
- polylines of 2–65 finite 3D points, in the same metre-scale coordinates as the
903
- input geometry. The nearest guide is projected onto the surface and given a
904
- consistent helical lay. Without a usable projected guide, a finite tangent
905
- fallback is used. Four nearby roots form a tuft within 1 mm of its anchor, with
906
- small angular variation, shallow lift and tapered tips. Sampling remains stable
907
- when fray changes; changing guides reauthors the groom.
908
-
909
- [native-crate-groom.js](https://github.com/Plasius-LTD/gpu-shared/tree/main/src/material-study-helpers/native-crate-groom.js) supplies the hash-verified
910
- fixture's loop/knot paths. Pass the same `heightOffset` used to ground its vertices
911
- (default 0 means unmodified asset coordinates); other transforms require matching
912
- transformed guide coordinates. The crate adapter adds 84,000 triangles: 90,578
913
- triangles / 271,734 vertices total, within the native default budget. Its final
914
- fibre arrays occupy 14,112,000 bytes. This deliberate denser nap replaces the
915
- previous 1,600-strand loose-end study. See [ADR-0035](docs/adrs/adr-0035-groomed-rope-and-bare-coat-endpoint.md).
916
-
917
-
918
- ### In-scene material inspection camera
919
-
920
- The shared [native-demo-camera.js](https://github.com/Plasius-LTD/gpu-shared/tree/main/src/material-study-helpers/native-demo-camera.js) adapter uses
921
- released `@plasius/gpu-camera` state/control/projection APIs and the separate
922
- `@plasius/gpu-camera-controls` pointer gesture runtime/browser normalizer. It maps DOM input to
923
- orbit, camera-plane truck/pedestal, pan/tilt, dolly and optical zoom; camera math
924
- is not duplicated in the renderer. The camera and fixture adapters now belong to the isolated gpu-shared material-study
925
- entry; the renderer root does not load them.
926
-
927
- ```js
928
- import { createNativeDemoCamera } from '@plasius/gpu-shared/material-study';
929
-
930
- // Supply a focusable canvas with an accessible name and persistent
931
- // aria-describedby instructions. The visual example is initially hidden,
932
- // aria-hidden, pointer-events:none and contains no interactive controls.
933
- const cameraControls = createNativeDemoCamera({
934
- canvas,
935
- camera: { eye: [-0.7, 0.5, 0.8], target: [0, 0.16, 0], fov: 42 },
936
- onChange: () => draw(),
937
- onHintChange: visible => { gestureExample.hidden = !visible; },
938
- });
939
- // Each draw uses camera: cameraControls.read(). Keep the controls alive across
940
- // renderer/material rebuilds; defer drawing while the renderer is unavailable.
941
- // At teardown (also cancels the hint timer):
942
- cameraControls.destroy();
943
- ```
944
-
945
- The material review uses direct manipulation with no persistent camera toolbar.
946
- The optional `onHintChange` callback shows a brief example on canvas focus and
947
- hides it after five seconds, on blur, Escape or disposal. Refocus or `?` replays
948
- it; replay replaces the prior timer. The host owns visual presentation: show
949
- mouse/touch or keyboard examples, preserve a screen-reader description without a
950
- time limit, and respect reduced motion. Do not move the camera to demonstrate a
951
- gesture or let the example intercept input. Keep material controls below the scene.
952
-
953
- | Input | Camera action |
954
- | --- | --- |
955
- | Left-drag / one finger | Orbit, with the grabbed surface following the drag on both axes |
956
- | Right/middle/Shift-drag | Truck / pedestal across the current view |
957
- | Alt-drag | Pan / tilt in place |
958
- | Scroll / touch pinch | Move closer or farther (dolly) |
959
- | Shift-scroll | Change the lens (optical zoom) |
960
- | Two fingers moving together | Truck / pedestal |
961
- | Arrows / Shift-arrows | Selected keyboard mode / truck |
962
- | 1–5 | Select orbit, truck, pan, dolly or lens zoom for keyboard arrows |
963
- | +/− | Dolly (lens zoom in keyboard zoom mode) |
964
- | Home | Reset view |
965
- | ? / Escape | Replay / dismiss the brief guide |
966
-
967
- Vertical orbit follows the grab direction for mouse, one-finger touch and keyboard
968
- arrows. The adapter maps single-pointer orbit through gpu-camera; the shared
969
- gesture runtime retains original pointer coordinates for multi-pointer truck/pinch
970
- and stable transitions. Keyboard mode selection does not change mouse or touch gestures. The source-only
971
- `setMode`, `getMode`, `move` and `reset` methods remain available to host code.
972
- Browser Ctrl/Meta/Alt key shortcuts and keys outside the canvas are left alone.
973
- Wheel input only belongs to a focused canvas; the surrounding page remains
974
- scrollable. No inertia or animation loop is introduced. Distances, lens and travel
975
- are bounded; pointer cancellation/blur/disposal release captures and listeners.
976
- This is an inspection camera without scene collision detection or object picking.
977
-
978
- Native material normal-map degeneracy checks now use normalized screen-space
979
- position/UV gradients. Valid tiny derivatives in rope macro views no longer disable
980
- normal maps. Mirrored UV handedness and geometric-normal fallback for degenerate
981
- UVs are retained in the common direct/reflection shader. The shader contract tests
982
- require browser rendering evidence as well; Node mocks do not execute WGSL.
983
- See [ADR-0036](docs/adrs/adr-0036-in-scene-material-inspection.md).
984
-
985
- ### Shared material-study runtime
986
-
987
- Production demo helpers and their regression tests now live in `@plasius/gpu-shared/material-study`. The renderer exposes `validateNativeSurfaceMaterials(materials)` so authoring and GPU creation use identical validation without allocation. See [ADR-0037](docs/adrs/adr-0037-shared-material-study-runtime.md).
988
-
989
- ## Progressive still-frame refinement
990
-
991
- Create the wavefront renderer with `progressive: true` to retain linear radiance across frames. `samplesPerPixel` at creation is the maximum batch capacity; pass a smaller value to `renderOnce({ samplesPerPixel })` or `renderFrame({ samplesPerPixel })`. Submit batches serially. `renderFrame()` awaits completion by default; with `renderOnce()`, await `renderer.device.queue.onSubmittedWorkDone()` before reporting completed samples. `getSnapshot().accumulatedSamplesPerPixel` reports submitted history samples.
992
-
993
- Camera or scene updates reset history automatically; `resetAccumulation()` also discards it without reallocating. Do not call `updateCamera()` between refinement passes unless the view changed. Progressive mode uses independent frame seeds, overriding static-frame stable sample routing. Denoising is applied after accumulation. Leave `progressive` unset for the existing independent-frame path. See [ADR 0038](docs/adrs/adr-0038-progressive-linear-radiance.md) for memory costs and host rollout responsibilities.