@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-renderer",
3
- "version": "0.1.15",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-agnostic WebGPU renderer runtime for Plasius projects.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/src/index.d.ts CHANGED
@@ -162,6 +162,69 @@ export interface WavefrontMeshAcceleration {
162
162
  readonly triangles: readonly WavefrontTriangleRecord[];
163
163
  }
164
164
 
165
+ export interface WavefrontReferenceRay {
166
+ readonly rayId: number;
167
+ readonly parentRayId: number;
168
+ readonly sourcePixelId: number;
169
+ readonly sampleId: number;
170
+ readonly bounce: number;
171
+ readonly mediumRefId: number;
172
+ readonly flags: number;
173
+ readonly origin: readonly [number, number, number] | readonly number[];
174
+ readonly direction: readonly [number, number, number] | readonly number[];
175
+ readonly throughput: readonly [number, number, number, number] | readonly number[];
176
+ readonly pixelX: number;
177
+ readonly pixelY: number;
178
+ }
179
+
180
+ export interface WavefrontReferenceTile {
181
+ readonly x?: number;
182
+ readonly y?: number;
183
+ readonly width?: number;
184
+ readonly height?: number;
185
+ }
186
+
187
+ export interface CreateWavefrontReferenceRayOptions {
188
+ readonly tile?: WavefrontReferenceTile;
189
+ readonly pixelIndex?: number;
190
+ readonly sampleIndex?: number;
191
+ readonly frameIndex?: number;
192
+ readonly jitterScale?: number;
193
+ }
194
+
195
+ export interface WavefrontReferenceHit {
196
+ readonly hitType: "surface" | "environment";
197
+ readonly rayId: number;
198
+ readonly sourcePixelId: number;
199
+ readonly distance: number;
200
+ readonly entityId: number;
201
+ readonly instanceId: number;
202
+ readonly primitiveId: number;
203
+ readonly materialId: number;
204
+ readonly materialRefId: number;
205
+ readonly mediumRefId: number;
206
+ readonly barycentrics: readonly [number, number, number] | readonly number[];
207
+ readonly uv: readonly [number, number] | readonly number[];
208
+ readonly geometricNormal: readonly [number, number, number] | readonly number[];
209
+ readonly shadingNormal: readonly [number, number, number] | readonly number[];
210
+ readonly frontFace: boolean;
211
+ readonly triangleIndex: number;
212
+ readonly triangleId: number;
213
+ readonly position: readonly [number, number, number] | readonly number[];
214
+ readonly color: readonly number[];
215
+ readonly emission: readonly number[];
216
+ readonly material: readonly number[];
217
+ }
218
+
219
+ export interface IntersectWavefrontReferenceTriangleOptions {
220
+ readonly maxDistance?: number;
221
+ readonly triangleIndex?: number;
222
+ }
223
+
224
+ export interface TraceWavefrontReferenceTrianglesOptions {
225
+ readonly maxDistance?: number;
226
+ }
227
+
165
228
  export type WavefrontAccelerationBuildMode = "gpu" | "cpu-debug";
166
229
 
167
230
  export interface WavefrontGpuMeshSource {
@@ -490,6 +553,21 @@ export function createWavefrontBvhSortStages(
490
553
  export function createWavefrontPathTracingComputeConfig(
491
554
  options?: CreateWavefrontPathTracingComputeRendererOptions
492
555
  ): WavefrontPathTracingComputeConfig;
556
+ export function createWavefrontReferenceRay(
557
+ config: WavefrontPathTracingComputeConfig,
558
+ options?: CreateWavefrontReferenceRayOptions
559
+ ): WavefrontReferenceRay;
560
+ export function intersectWavefrontReferenceTriangle(
561
+ ray: WavefrontReferenceRay,
562
+ triangle: WavefrontTriangleRecord,
563
+ options?: IntersectWavefrontReferenceTriangleOptions
564
+ ): WavefrontReferenceHit | null;
565
+ export function traceWavefrontReferenceTriangles(
566
+ config: WavefrontPathTracingComputeConfig,
567
+ ray: WavefrontReferenceRay,
568
+ triangles: readonly WavefrontTriangleRecord[],
569
+ options?: TraceWavefrontReferenceTrianglesOptions
570
+ ): WavefrontReferenceHit;
493
571
  export function packWavefrontSceneObjects(
494
572
  sceneObjects: readonly WavefrontSceneObjectInput[],
495
573
  capacity?: number
package/src/index.js CHANGED
@@ -9,13 +9,16 @@ export {
9
9
  createWavefrontMeshAcceleration,
10
10
  createWavefrontPathTracingComputeConfig,
11
11
  createWavefrontPathTracingComputeRenderer,
12
+ createWavefrontReferenceRay,
12
13
  estimateWavefrontPathTracingMemory,
14
+ intersectWavefrontReferenceTriangle,
13
15
  normalizeWavefrontMesh,
14
16
  normalizeWavefrontSceneObject,
15
17
  packWavefrontBvhNodes,
16
18
  packWavefrontSceneObjects,
17
19
  packWavefrontTriangles,
18
20
  supportsWavefrontPathTracingCompute,
21
+ traceWavefrontReferenceTriangles,
19
22
  wavefrontMaterialKinds,
20
23
  wavefrontPathTracingComputeLimits,
21
24
  wavefrontSceneObjectKinds,
@@ -194,6 +194,33 @@ function normalize(value, fallback = [0, 0, 1]) {
194
194
  return [value[0] / length, value[1] / length, value[2] / length];
195
195
  }
196
196
 
197
+ function hashUint32(value) {
198
+ let x = value >>> 0;
199
+ x = ((((x >>> 16) ^ x) >>> 0) * 0x45d9f3b) >>> 0;
200
+ x = ((((x >>> 16) ^ x) >>> 0) * 0x45d9f3b) >>> 0;
201
+ return ((x >>> 16) ^ x) >>> 0;
202
+ }
203
+
204
+ function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
205
+ let x =
206
+ ((pixelId >>> 0) * 747796405) ^
207
+ ((sampleId >>> 0) * 2891336453) ^
208
+ ((bounce >>> 0) * 277803737) ^
209
+ ((frameIndex >>> 0) * 1442695041) ^
210
+ ((dimension >>> 0) * 1597334677);
211
+ x >>>= 0;
212
+ x ^= x >>> 16;
213
+ x = (x * 0x7feb352d) >>> 0;
214
+ x ^= x >>> 15;
215
+ x = (x * 0x846ca68b) >>> 0;
216
+ x ^= x >>> 16;
217
+ return x >>> 0;
218
+ }
219
+
220
+ function random01FromSeed(seed) {
221
+ return (hashUint32(seed) & 0x00ffffff) / 16777215;
222
+ }
223
+
197
224
  function getArrayLikeLength(value) {
198
225
  return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
199
226
  }
@@ -882,6 +909,36 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
882
909
  });
883
910
  }
884
911
 
912
+ function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
913
+ void origin;
914
+ const rayDirection = normalize(direction, [0, 1, 0]);
915
+ const upFactor = clamp(rayDirection[1] * 0.5 + 0.5, 0, 1);
916
+ const sunDirection = normalize(
917
+ config.environmentLighting?.sunDirection ?? DEFAULT_ENVIRONMENT_LIGHTING.sunDirection,
918
+ DEFAULT_ENVIRONMENT_LIGHTING.sunDirection
919
+ );
920
+ const sunGlow = Math.pow(clamp(dot(rayDirection, sunDirection), 0, 1), 192);
921
+ const horizonColor =
922
+ config.environmentLighting?.horizonColor ?? DEFAULT_ENVIRONMENT_LIGHTING.horizonColor;
923
+ const zenithColor =
924
+ config.environmentLighting?.zenithColor ?? DEFAULT_ENVIRONMENT_LIGHTING.zenithColor;
925
+ const sunColor = config.environmentLighting?.sunColor ?? DEFAULT_ENVIRONMENT_LIGHTING.sunColor;
926
+ const intensity = Math.max(
927
+ 0.0001,
928
+ Number(config.environmentLighting?.intensity ?? DEFAULT_ENVIRONMENT_LIGHTING.intensity)
929
+ );
930
+
931
+ return Object.freeze([
932
+ (horizonColor[0] * (1 - upFactor) + zenithColor[0] * upFactor + sunColor[0] * sunGlow) *
933
+ intensity,
934
+ (horizonColor[1] * (1 - upFactor) + zenithColor[1] * upFactor + sunColor[1] * sunGlow) *
935
+ intensity,
936
+ (horizonColor[2] * (1 - upFactor) + zenithColor[2] * upFactor + sunColor[2] * sunGlow) *
937
+ intensity,
938
+ 1,
939
+ ]);
940
+ }
941
+
885
942
  function resolveEnvironmentPortalMode(value, hasPortals) {
886
943
  if (value === undefined || value === null) {
887
944
  return hasPortals ? 2 : 0;
@@ -1489,6 +1546,229 @@ function createTiles(width, height, tileSize) {
1489
1546
  return Object.freeze(tiles);
1490
1547
  }
1491
1548
 
1549
+ function normalizeReferenceTile(config, tileInput = {}) {
1550
+ const tileX = clamp(
1551
+ readNonNegativeInteger("tile.x", tileInput.x, 0),
1552
+ 0,
1553
+ Math.max(0, config.width - 1)
1554
+ );
1555
+ const tileY = clamp(
1556
+ readNonNegativeInteger("tile.y", tileInput.y, 0),
1557
+ 0,
1558
+ Math.max(0, config.height - 1)
1559
+ );
1560
+ const tileWidth = clamp(
1561
+ readPositiveInteger("tile.width", tileInput.width, config.width - tileX),
1562
+ 1,
1563
+ config.width - tileX
1564
+ );
1565
+ const tileHeight = clamp(
1566
+ readPositiveInteger("tile.height", tileInput.height, config.height - tileY),
1567
+ 1,
1568
+ config.height - tileY
1569
+ );
1570
+
1571
+ return Object.freeze({
1572
+ x: tileX,
1573
+ y: tileY,
1574
+ width: tileWidth,
1575
+ height: tileHeight,
1576
+ });
1577
+ }
1578
+
1579
+ function repairReferenceShadingNormal(geometricNormal, shadingNormal) {
1580
+ const normal = normalize(shadingNormal, geometricNormal);
1581
+ return dot(normal, geometricNormal) < 0 ? scale(normal, -1) : normal;
1582
+ }
1583
+
1584
+ function readOptionalMaxDistance(value) {
1585
+ if (value === undefined || value === null) {
1586
+ return Number.POSITIVE_INFINITY;
1587
+ }
1588
+ const numeric = Number(value);
1589
+ if (!Number.isFinite(numeric) || numeric <= 0) {
1590
+ throw new Error("maxDistance must be a positive finite number when provided.");
1591
+ }
1592
+ return numeric;
1593
+ }
1594
+
1595
+ export function createWavefrontReferenceRay(config, options = {}) {
1596
+ if (!config || typeof config !== "object") {
1597
+ throw new Error("config must be a wavefront path tracing config.");
1598
+ }
1599
+
1600
+ const tile = normalizeReferenceTile(config, options.tile);
1601
+ const tilePixelCount = tile.width * tile.height;
1602
+ const pixelIndex = readNonNegativeInteger("pixelIndex", options.pixelIndex, 0);
1603
+ if (pixelIndex >= tilePixelCount) {
1604
+ throw new Error(`pixelIndex ${pixelIndex} exceeds tile capacity ${tilePixelCount}.`);
1605
+ }
1606
+
1607
+ const sampleIndex = readNonNegativeInteger("sampleIndex", options.sampleIndex, 0);
1608
+ const frameIndex = readNonNegativeInteger("frameIndex", options.frameIndex, config.frameIndex ?? 0);
1609
+ const jitterScale = clamp(readFiniteNumber("jitterScale", options.jitterScale, 0.35), 0, 1);
1610
+ const localX = pixelIndex % tile.width;
1611
+ const localY = Math.floor(pixelIndex / tile.width);
1612
+ const pixelX = tile.x + localX;
1613
+ const pixelY = tile.y + localY;
1614
+ const sourcePixelId = pixelY * config.width + pixelX;
1615
+ const jitterX = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 1)) - 0.5;
1616
+ const jitterY = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 2)) - 0.5;
1617
+ const ndcX = ((pixelX + 0.5 + jitterX * jitterScale) / config.width) * 2 - 1;
1618
+ const ndcY = 1 - ((pixelY + 0.5 + jitterY * jitterScale) / config.height) * 2;
1619
+ const viewX = ndcX * config.camera.tanHalfFovY * config.camera.aspect;
1620
+ const viewY = ndcY * config.camera.tanHalfFovY;
1621
+ const direction = normalize(
1622
+ add(
1623
+ add(config.camera.forward, scale(config.camera.right, viewX)),
1624
+ scale(config.camera.up, viewY)
1625
+ ),
1626
+ config.camera.forward
1627
+ );
1628
+
1629
+ return Object.freeze({
1630
+ rayId: pixelIndex,
1631
+ parentRayId: 0xffffffff,
1632
+ sourcePixelId,
1633
+ sampleId: sampleIndex,
1634
+ bounce: 0,
1635
+ mediumRefId: 0,
1636
+ flags: 0,
1637
+ origin: Object.freeze([...config.camera.position]),
1638
+ direction: Object.freeze(direction),
1639
+ throughput: Object.freeze([1, 1, 1, 1]),
1640
+ pixelX,
1641
+ pixelY,
1642
+ });
1643
+ }
1644
+
1645
+ export function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1646
+ if (!ray || typeof ray !== "object") {
1647
+ throw new Error("ray must be a wavefront reference ray.");
1648
+ }
1649
+ if (!triangle || typeof triangle !== "object") {
1650
+ throw new Error("triangle must be a wavefront triangle record.");
1651
+ }
1652
+
1653
+ const maxDistance = readOptionalMaxDistance(options.maxDistance);
1654
+ const triangleIndex = readNonNegativeInteger("triangleIndex", options.triangleIndex, 0);
1655
+ const edge1 = subtract(triangle.v1, triangle.v0);
1656
+ const edge2 = subtract(triangle.v2, triangle.v0);
1657
+ const pvec = cross(ray.direction, edge2);
1658
+ const determinant = dot(edge1, pvec);
1659
+ if (Math.abs(determinant) < 0.0000001) {
1660
+ return null;
1661
+ }
1662
+
1663
+ const invDet = 1 / determinant;
1664
+ const tvec = subtract(ray.origin, triangle.v0);
1665
+ const u = dot(tvec, pvec) * invDet;
1666
+ if (u < 0 || u > 1) {
1667
+ return null;
1668
+ }
1669
+
1670
+ const qvec = cross(tvec, edge1);
1671
+ const v = dot(ray.direction, qvec) * invDet;
1672
+ if (v < 0 || u + v > 1) {
1673
+ return null;
1674
+ }
1675
+
1676
+ const distance = dot(edge2, qvec) * invDet;
1677
+ if (distance <= 0.001 || distance > maxDistance) {
1678
+ return null;
1679
+ }
1680
+
1681
+ const geometric = normalize(cross(edge1, edge2), [0, 1, 0]);
1682
+ const frontFace = dot(ray.direction, geometric) < 0;
1683
+ const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
1684
+ const w = 1 - u - v;
1685
+ const interpolated = [
1686
+ triangle.n0[0] * w + triangle.n1[0] * u + triangle.n2[0] * v,
1687
+ triangle.n0[1] * w + triangle.n1[1] * u + triangle.n2[1] * v,
1688
+ triangle.n0[2] * w + triangle.n1[2] * u + triangle.n2[2] * v,
1689
+ ];
1690
+ const shadingNormal = repairReferenceShadingNormal(orientedGeometric, interpolated);
1691
+ const uv = [
1692
+ triangle.uv0[0] * w + triangle.uv1[0] * u + triangle.uv2[0] * v,
1693
+ triangle.uv0[1] * w + triangle.uv1[1] * u + triangle.uv2[1] * v,
1694
+ ];
1695
+ const position = add(ray.origin, scale(ray.direction, distance));
1696
+
1697
+ return Object.freeze({
1698
+ hitType: "surface",
1699
+ rayId: ray.rayId,
1700
+ sourcePixelId: ray.sourcePixelId,
1701
+ distance,
1702
+ entityId: triangle.meshId,
1703
+ instanceId: 0,
1704
+ primitiveId: triangle.triangleId,
1705
+ materialId: triangle.materialKind,
1706
+ materialRefId: triangle.materialRefId,
1707
+ mediumRefId: triangle.mediumRefId,
1708
+ barycentrics: Object.freeze([w, u, v]),
1709
+ uv: Object.freeze(uv),
1710
+ geometricNormal: Object.freeze(orientedGeometric),
1711
+ shadingNormal: Object.freeze(shadingNormal),
1712
+ frontFace,
1713
+ triangleIndex,
1714
+ triangleId: triangle.triangleId,
1715
+ position: Object.freeze(position),
1716
+ color: triangle.color,
1717
+ emission: triangle.emission,
1718
+ material: triangle.material,
1719
+ });
1720
+ }
1721
+
1722
+ function createWavefrontReferenceEnvironmentHit(config, ray) {
1723
+ const radiance = evaluateReferenceEnvironmentRadiance(config, ray.origin, ray.direction);
1724
+ return Object.freeze({
1725
+ hitType: "environment",
1726
+ rayId: ray.rayId,
1727
+ sourcePixelId: ray.sourcePixelId,
1728
+ distance: -1,
1729
+ entityId: 0,
1730
+ instanceId: 0,
1731
+ primitiveId: 0,
1732
+ materialId: 0,
1733
+ materialRefId: 0,
1734
+ mediumRefId: 0,
1735
+ barycentrics: Object.freeze([0, 0, 0]),
1736
+ uv: Object.freeze([0, 0]),
1737
+ geometricNormal: Object.freeze(scale(ray.direction, -1)),
1738
+ shadingNormal: Object.freeze(scale(ray.direction, -1)),
1739
+ frontFace: true,
1740
+ triangleIndex: -1,
1741
+ triangleId: -1,
1742
+ position: Object.freeze(add(ray.origin, scale(ray.direction, 1000))),
1743
+ color: Object.freeze([0, 0, 0, 0]),
1744
+ emission: radiance,
1745
+ material: Object.freeze([1, 0, 1, 1]),
1746
+ });
1747
+ }
1748
+
1749
+ export function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
1750
+ if (!config || typeof config !== "object") {
1751
+ throw new Error("config must be a wavefront path tracing config.");
1752
+ }
1753
+
1754
+ const source = Array.isArray(triangles) ? triangles : [];
1755
+ let nearestHit = null;
1756
+ let nearestDistance = readOptionalMaxDistance(options.maxDistance);
1757
+
1758
+ source.forEach((triangle, index) => {
1759
+ const hit = intersectWavefrontReferenceTriangle(ray, triangle, {
1760
+ maxDistance: Number.isFinite(nearestDistance) ? nearestDistance : undefined,
1761
+ triangleIndex: index,
1762
+ });
1763
+ if (hit && hit.distance < nearestDistance) {
1764
+ nearestDistance = hit.distance;
1765
+ nearestHit = hit;
1766
+ }
1767
+ });
1768
+
1769
+ return nearestHit ?? createWavefrontReferenceEnvironmentHit(config, ray);
1770
+ }
1771
+
1492
1772
  function clampTileSizeForDevice(config, device) {
1493
1773
  const limit = Number(device?.limits?.maxStorageBufferBindingSize);
1494
1774
  if (!Number.isFinite(limit) || limit <= 0) {