@plasius/gpu-renderer 0.1.15 → 0.2.0

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/CHANGELOG.md CHANGED
@@ -23,10 +23,12 @@ All notable changes to this project will be documented in this file.
23
23
  - **Security**
24
24
  - (placeholder)
25
25
 
26
- ## [0.1.15] - 2026-06-04
26
+ ## [0.2.0] - 2026-06-05
27
27
 
28
28
  - **Added**
29
- - (placeholder)
29
+ - Added deterministic wavefront reference helpers for primary-ray creation,
30
+ triangle-hit evaluation, nearest-hit selection, and environment-miss
31
+ fallback alongside regression tests for task-level acceptance coverage.
30
32
 
31
33
  - **Changed**
32
34
  - (placeholder)
@@ -287,4 +289,4 @@ All notable changes to this project will be documented in this file.
287
289
  [0.1.11]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.1.11
288
290
  [0.1.12]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.1.12
289
291
  [0.1.14]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.1.14
290
- [0.1.15]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.1.15
292
+ [0.2.0]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.2.0
package/README.md CHANGED
@@ -242,6 +242,9 @@ renderer.bindXrManager(xr, {
242
242
  - `createRayTracingRenderPlan(options)`
243
243
  - `createWavefrontPathTracingComputeRenderer(options)`
244
244
  - `createWavefrontPathTracingComputeConfig(options)`
245
+ - `createWavefrontReferenceRay(config, options?)`
246
+ - `intersectWavefrontReferenceTriangle(ray, triangle, options?)`
247
+ - `traceWavefrontReferenceTriangles(config, ray, triangles, options?)`
245
248
  - `normalizeWavefrontMesh(input)`
246
249
  - `createWavefrontGpuMeshSource(meshes)`
247
250
  - `createWavefrontBvhSortStages(itemCount)`
@@ -260,6 +263,11 @@ renderer.bindXrManager(xr, {
260
263
  - `rendererWorkerProfileNames`
261
264
  - `rendererWorkerManifests`
262
265
 
266
+ The reference helpers mirror the renderer WGSL camera and triangle-hit math in
267
+ deterministic JavaScript so tests and downstream tooling can validate primary
268
+ ray generation, barycentrics, nearest-hit selection, and environment misses
269
+ without standing up a WebGPU device.
270
+
263
271
  ## Demo
264
272
 
265
273
  Run the demo server from the repo root:
package/dist/index.cjs CHANGED
@@ -32,11 +32,13 @@ __export(index_exports, {
32
32
  createWavefrontPathTracingComputeConfig: () => createWavefrontPathTracingComputeConfig,
33
33
  createWavefrontPathTracingComputeRenderer: () => createWavefrontPathTracingComputeRenderer,
34
34
  createWavefrontPathTracingPlan: () => createWavefrontPathTracingPlan,
35
+ createWavefrontReferenceRay: () => createWavefrontReferenceRay,
35
36
  defaultRendererClearColor: () => defaultRendererClearColor,
36
37
  defaultRendererWorkerProfile: () => defaultRendererWorkerProfile,
37
38
  estimateWavefrontPathTracingMemory: () => estimateWavefrontPathTracingMemory,
38
39
  getRendererWorkerManifest: () => getRendererWorkerManifest,
39
40
  getRendererWorkerProfile: () => getRendererWorkerProfile,
41
+ intersectWavefrontReferenceTriangle: () => intersectWavefrontReferenceTriangle,
40
42
  normalizeWavefrontMesh: () => normalizeWavefrontMesh,
41
43
  normalizeWavefrontSceneObject: () => normalizeWavefrontSceneObject,
42
44
  packWavefrontBvhNodes: () => packWavefrontBvhNodes,
@@ -56,6 +58,7 @@ __export(index_exports, {
56
58
  rendererWorkerQueueClass: () => rendererWorkerQueueClass,
57
59
  supportsWavefrontPathTracingCompute: () => supportsWavefrontPathTracingCompute,
58
60
  supportsWebGpu: () => supportsWebGpu,
61
+ traceWavefrontReferenceTriangles: () => traceWavefrontReferenceTriangles,
59
62
  wavefrontMaterialKinds: () => wavefrontMaterialKinds,
60
63
  wavefrontPathTracingComputeLimits: () => wavefrontPathTracingComputeLimits,
61
64
  wavefrontSceneObjectKinds: () => wavefrontSceneObjectKinds
@@ -231,6 +234,25 @@ function normalize(value, fallback = [0, 0, 1]) {
231
234
  }
232
235
  return [value[0] / length, value[1] / length, value[2] / length];
233
236
  }
237
+ function hashUint32(value) {
238
+ let x = value >>> 0;
239
+ x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
240
+ x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
241
+ return (x >>> 16 ^ x) >>> 0;
242
+ }
243
+ function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
244
+ let x = (pixelId >>> 0) * 747796405 ^ (sampleId >>> 0) * 2891336453 ^ (bounce >>> 0) * 277803737 ^ (frameIndex >>> 0) * 1442695041 ^ (dimension >>> 0) * 1597334677;
245
+ x >>>= 0;
246
+ x ^= x >>> 16;
247
+ x = x * 2146121005 >>> 0;
248
+ x ^= x >>> 15;
249
+ x = x * 2221713035 >>> 0;
250
+ x ^= x >>> 16;
251
+ return x >>> 0;
252
+ }
253
+ function random01FromSeed(seed) {
254
+ return (hashUint32(seed) & 16777215) / 16777215;
255
+ }
234
256
  function getArrayLikeLength(value) {
235
257
  return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
236
258
  }
@@ -829,6 +851,29 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
829
851
  exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
830
852
  });
831
853
  }
854
+ function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
855
+ void origin;
856
+ const rayDirection = normalize(direction, [0, 1, 0]);
857
+ const upFactor = clamp(rayDirection[1] * 0.5 + 0.5, 0, 1);
858
+ const sunDirection = normalize(
859
+ config.environmentLighting?.sunDirection ?? DEFAULT_ENVIRONMENT_LIGHTING.sunDirection,
860
+ DEFAULT_ENVIRONMENT_LIGHTING.sunDirection
861
+ );
862
+ const sunGlow = Math.pow(clamp(dot(rayDirection, sunDirection), 0, 1), 192);
863
+ const horizonColor = config.environmentLighting?.horizonColor ?? DEFAULT_ENVIRONMENT_LIGHTING.horizonColor;
864
+ const zenithColor = config.environmentLighting?.zenithColor ?? DEFAULT_ENVIRONMENT_LIGHTING.zenithColor;
865
+ const sunColor = config.environmentLighting?.sunColor ?? DEFAULT_ENVIRONMENT_LIGHTING.sunColor;
866
+ const intensity = Math.max(
867
+ 1e-4,
868
+ Number(config.environmentLighting?.intensity ?? DEFAULT_ENVIRONMENT_LIGHTING.intensity)
869
+ );
870
+ return Object.freeze([
871
+ (horizonColor[0] * (1 - upFactor) + zenithColor[0] * upFactor + sunColor[0] * sunGlow) * intensity,
872
+ (horizonColor[1] * (1 - upFactor) + zenithColor[1] * upFactor + sunColor[1] * sunGlow) * intensity,
873
+ (horizonColor[2] * (1 - upFactor) + zenithColor[2] * upFactor + sunColor[2] * sunGlow) * intensity,
874
+ 1
875
+ ]);
876
+ }
832
877
  function resolveEnvironmentPortalMode(value, hasPortals) {
833
878
  if (value === void 0 || value === null) {
834
879
  return hasPortals ? 2 : 0;
@@ -1357,6 +1402,209 @@ function createTiles(width, height, tileSize) {
1357
1402
  }
1358
1403
  return Object.freeze(tiles);
1359
1404
  }
1405
+ function normalizeReferenceTile(config, tileInput = {}) {
1406
+ const tileX = clamp(
1407
+ readNonNegativeInteger("tile.x", tileInput.x, 0),
1408
+ 0,
1409
+ Math.max(0, config.width - 1)
1410
+ );
1411
+ const tileY = clamp(
1412
+ readNonNegativeInteger("tile.y", tileInput.y, 0),
1413
+ 0,
1414
+ Math.max(0, config.height - 1)
1415
+ );
1416
+ const tileWidth = clamp(
1417
+ readPositiveInteger("tile.width", tileInput.width, config.width - tileX),
1418
+ 1,
1419
+ config.width - tileX
1420
+ );
1421
+ const tileHeight = clamp(
1422
+ readPositiveInteger("tile.height", tileInput.height, config.height - tileY),
1423
+ 1,
1424
+ config.height - tileY
1425
+ );
1426
+ return Object.freeze({
1427
+ x: tileX,
1428
+ y: tileY,
1429
+ width: tileWidth,
1430
+ height: tileHeight
1431
+ });
1432
+ }
1433
+ function repairReferenceShadingNormal(geometricNormal, shadingNormal) {
1434
+ const normal = normalize(shadingNormal, geometricNormal);
1435
+ return dot(normal, geometricNormal) < 0 ? scale(normal, -1) : normal;
1436
+ }
1437
+ function readOptionalMaxDistance(value) {
1438
+ if (value === void 0 || value === null) {
1439
+ return Number.POSITIVE_INFINITY;
1440
+ }
1441
+ const numeric = Number(value);
1442
+ if (!Number.isFinite(numeric) || numeric <= 0) {
1443
+ throw new Error("maxDistance must be a positive finite number when provided.");
1444
+ }
1445
+ return numeric;
1446
+ }
1447
+ function createWavefrontReferenceRay(config, options = {}) {
1448
+ if (!config || typeof config !== "object") {
1449
+ throw new Error("config must be a wavefront path tracing config.");
1450
+ }
1451
+ const tile = normalizeReferenceTile(config, options.tile);
1452
+ const tilePixelCount = tile.width * tile.height;
1453
+ const pixelIndex = readNonNegativeInteger("pixelIndex", options.pixelIndex, 0);
1454
+ if (pixelIndex >= tilePixelCount) {
1455
+ throw new Error(`pixelIndex ${pixelIndex} exceeds tile capacity ${tilePixelCount}.`);
1456
+ }
1457
+ const sampleIndex = readNonNegativeInteger("sampleIndex", options.sampleIndex, 0);
1458
+ const frameIndex = readNonNegativeInteger("frameIndex", options.frameIndex, config.frameIndex ?? 0);
1459
+ const jitterScale = clamp(readFiniteNumber("jitterScale", options.jitterScale, 0.35), 0, 1);
1460
+ const localX = pixelIndex % tile.width;
1461
+ const localY = Math.floor(pixelIndex / tile.width);
1462
+ const pixelX = tile.x + localX;
1463
+ const pixelY = tile.y + localY;
1464
+ const sourcePixelId = pixelY * config.width + pixelX;
1465
+ const jitterX = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 1)) - 0.5;
1466
+ const jitterY = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 2)) - 0.5;
1467
+ const ndcX = (pixelX + 0.5 + jitterX * jitterScale) / config.width * 2 - 1;
1468
+ const ndcY = 1 - (pixelY + 0.5 + jitterY * jitterScale) / config.height * 2;
1469
+ const viewX = ndcX * config.camera.tanHalfFovY * config.camera.aspect;
1470
+ const viewY = ndcY * config.camera.tanHalfFovY;
1471
+ const direction = normalize(
1472
+ add(
1473
+ add(config.camera.forward, scale(config.camera.right, viewX)),
1474
+ scale(config.camera.up, viewY)
1475
+ ),
1476
+ config.camera.forward
1477
+ );
1478
+ return Object.freeze({
1479
+ rayId: pixelIndex,
1480
+ parentRayId: 4294967295,
1481
+ sourcePixelId,
1482
+ sampleId: sampleIndex,
1483
+ bounce: 0,
1484
+ mediumRefId: 0,
1485
+ flags: 0,
1486
+ origin: Object.freeze([...config.camera.position]),
1487
+ direction: Object.freeze(direction),
1488
+ throughput: Object.freeze([1, 1, 1, 1]),
1489
+ pixelX,
1490
+ pixelY
1491
+ });
1492
+ }
1493
+ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1494
+ if (!ray || typeof ray !== "object") {
1495
+ throw new Error("ray must be a wavefront reference ray.");
1496
+ }
1497
+ if (!triangle || typeof triangle !== "object") {
1498
+ throw new Error("triangle must be a wavefront triangle record.");
1499
+ }
1500
+ const maxDistance = readOptionalMaxDistance(options.maxDistance);
1501
+ const triangleIndex = readNonNegativeInteger("triangleIndex", options.triangleIndex, 0);
1502
+ const edge1 = subtract(triangle.v1, triangle.v0);
1503
+ const edge2 = subtract(triangle.v2, triangle.v0);
1504
+ const pvec = cross(ray.direction, edge2);
1505
+ const determinant = dot(edge1, pvec);
1506
+ if (Math.abs(determinant) < 1e-7) {
1507
+ return null;
1508
+ }
1509
+ const invDet = 1 / determinant;
1510
+ const tvec = subtract(ray.origin, triangle.v0);
1511
+ const u = dot(tvec, pvec) * invDet;
1512
+ if (u < 0 || u > 1) {
1513
+ return null;
1514
+ }
1515
+ const qvec = cross(tvec, edge1);
1516
+ const v = dot(ray.direction, qvec) * invDet;
1517
+ if (v < 0 || u + v > 1) {
1518
+ return null;
1519
+ }
1520
+ const distance = dot(edge2, qvec) * invDet;
1521
+ if (distance <= 1e-3 || distance > maxDistance) {
1522
+ return null;
1523
+ }
1524
+ const geometric = normalize(cross(edge1, edge2), [0, 1, 0]);
1525
+ const frontFace = dot(ray.direction, geometric) < 0;
1526
+ const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
1527
+ const w = 1 - u - v;
1528
+ const interpolated = [
1529
+ triangle.n0[0] * w + triangle.n1[0] * u + triangle.n2[0] * v,
1530
+ triangle.n0[1] * w + triangle.n1[1] * u + triangle.n2[1] * v,
1531
+ triangle.n0[2] * w + triangle.n1[2] * u + triangle.n2[2] * v
1532
+ ];
1533
+ const shadingNormal = repairReferenceShadingNormal(orientedGeometric, interpolated);
1534
+ const uv = [
1535
+ triangle.uv0[0] * w + triangle.uv1[0] * u + triangle.uv2[0] * v,
1536
+ triangle.uv0[1] * w + triangle.uv1[1] * u + triangle.uv2[1] * v
1537
+ ];
1538
+ const position = add(ray.origin, scale(ray.direction, distance));
1539
+ return Object.freeze({
1540
+ hitType: "surface",
1541
+ rayId: ray.rayId,
1542
+ sourcePixelId: ray.sourcePixelId,
1543
+ distance,
1544
+ entityId: triangle.meshId,
1545
+ instanceId: 0,
1546
+ primitiveId: triangle.triangleId,
1547
+ materialId: triangle.materialKind,
1548
+ materialRefId: triangle.materialRefId,
1549
+ mediumRefId: triangle.mediumRefId,
1550
+ barycentrics: Object.freeze([w, u, v]),
1551
+ uv: Object.freeze(uv),
1552
+ geometricNormal: Object.freeze(orientedGeometric),
1553
+ shadingNormal: Object.freeze(shadingNormal),
1554
+ frontFace,
1555
+ triangleIndex,
1556
+ triangleId: triangle.triangleId,
1557
+ position: Object.freeze(position),
1558
+ color: triangle.color,
1559
+ emission: triangle.emission,
1560
+ material: triangle.material
1561
+ });
1562
+ }
1563
+ function createWavefrontReferenceEnvironmentHit(config, ray) {
1564
+ const radiance = evaluateReferenceEnvironmentRadiance(config, ray.origin, ray.direction);
1565
+ return Object.freeze({
1566
+ hitType: "environment",
1567
+ rayId: ray.rayId,
1568
+ sourcePixelId: ray.sourcePixelId,
1569
+ distance: -1,
1570
+ entityId: 0,
1571
+ instanceId: 0,
1572
+ primitiveId: 0,
1573
+ materialId: 0,
1574
+ materialRefId: 0,
1575
+ mediumRefId: 0,
1576
+ barycentrics: Object.freeze([0, 0, 0]),
1577
+ uv: Object.freeze([0, 0]),
1578
+ geometricNormal: Object.freeze(scale(ray.direction, -1)),
1579
+ shadingNormal: Object.freeze(scale(ray.direction, -1)),
1580
+ frontFace: true,
1581
+ triangleIndex: -1,
1582
+ triangleId: -1,
1583
+ position: Object.freeze(add(ray.origin, scale(ray.direction, 1e3))),
1584
+ color: Object.freeze([0, 0, 0, 0]),
1585
+ emission: radiance,
1586
+ material: Object.freeze([1, 0, 1, 1])
1587
+ });
1588
+ }
1589
+ function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
1590
+ if (!config || typeof config !== "object") {
1591
+ throw new Error("config must be a wavefront path tracing config.");
1592
+ }
1593
+ const source = Array.isArray(triangles) ? triangles : [];
1594
+ let nearestHit = null;
1595
+ let nearestDistance = readOptionalMaxDistance(options.maxDistance);
1596
+ source.forEach((triangle, index) => {
1597
+ const hit = intersectWavefrontReferenceTriangle(ray, triangle, {
1598
+ maxDistance: Number.isFinite(nearestDistance) ? nearestDistance : void 0,
1599
+ triangleIndex: index
1600
+ });
1601
+ if (hit && hit.distance < nearestDistance) {
1602
+ nearestDistance = hit.distance;
1603
+ nearestHit = hit;
1604
+ }
1605
+ });
1606
+ return nearestHit ?? createWavefrontReferenceEnvironmentHit(config, ray);
1607
+ }
1360
1608
  function clampTileSizeForDevice(config, device) {
1361
1609
  const limit = Number(device?.limits?.maxStorageBufferBindingSize);
1362
1610
  if (!Number.isFinite(limit) || limit <= 0) {
@@ -4896,11 +5144,13 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
4896
5144
  createWavefrontPathTracingComputeConfig,
4897
5145
  createWavefrontPathTracingComputeRenderer,
4898
5146
  createWavefrontPathTracingPlan,
5147
+ createWavefrontReferenceRay,
4899
5148
  defaultRendererClearColor,
4900
5149
  defaultRendererWorkerProfile,
4901
5150
  estimateWavefrontPathTracingMemory,
4902
5151
  getRendererWorkerManifest,
4903
5152
  getRendererWorkerProfile,
5153
+ intersectWavefrontReferenceTriangle,
4904
5154
  normalizeWavefrontMesh,
4905
5155
  normalizeWavefrontSceneObject,
4906
5156
  packWavefrontBvhNodes,
@@ -4920,6 +5170,7 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
4920
5170
  rendererWorkerQueueClass,
4921
5171
  supportsWavefrontPathTracingCompute,
4922
5172
  supportsWebGpu,
5173
+ traceWavefrontReferenceTriangles,
4923
5174
  wavefrontMaterialKinds,
4924
5175
  wavefrontPathTracingComputeLimits,
4925
5176
  wavefrontSceneObjectKinds