@plasius/gpu-renderer 0.1.15 → 0.2.1
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 +29 -5
- package/README.md +25 -1
- package/dist/index.cjs +382 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +374 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/index.d.ts +146 -26
- package/src/index.js +8 -0
- package/src/wavefront-compute.js +407 -16
package/src/wavefront-compute.js
CHANGED
|
@@ -6,6 +6,9 @@ const DEFAULT_SAMPLES_PER_PIXEL = 1;
|
|
|
6
6
|
const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
|
|
7
7
|
const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
|
|
8
8
|
const WORKGROUP_SIZE = 64;
|
|
9
|
+
export const rendererWavefrontComputeMode = "webgpu-compute";
|
|
10
|
+
export const rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
|
|
11
|
+
export const rendererWavefrontComputeStatsStride = 8;
|
|
9
12
|
const RAY_RECORD_BYTES = 80;
|
|
10
13
|
const HIT_RECORD_BYTES = 208;
|
|
11
14
|
const SCENE_OBJECT_RECORD_BYTES = 96;
|
|
@@ -18,7 +21,9 @@ const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
|
|
|
18
21
|
const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
|
|
19
22
|
const ACCUMULATION_RECORD_BYTES = 16;
|
|
20
23
|
const CONFIG_BUFFER_BYTES = 272;
|
|
21
|
-
const
|
|
24
|
+
const COUNTER_DISPATCH_ARGS_OFFSET = 16;
|
|
25
|
+
const INDIRECT_DISPATCH_ARGS_BYTES = 12;
|
|
26
|
+
const COUNTER_BUFFER_BYTES = 32;
|
|
22
27
|
const TRACE_STORAGE_BUFFER_BINDINGS = 9;
|
|
23
28
|
const HIT_TYPE_SURFACE = 0;
|
|
24
29
|
const HIT_TYPE_EMISSIVE = 1;
|
|
@@ -64,6 +69,8 @@ export const wavefrontPathTracingComputeLimits = Object.freeze({
|
|
|
64
69
|
emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
|
|
65
70
|
environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
|
|
66
71
|
accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
|
|
72
|
+
counterRecordBytes: COUNTER_BUFFER_BYTES,
|
|
73
|
+
indirectDispatchRecordBytes: INDIRECT_DISPATCH_ARGS_BYTES,
|
|
67
74
|
});
|
|
68
75
|
|
|
69
76
|
export const wavefrontSceneObjectKinds = Object.freeze({
|
|
@@ -194,6 +201,33 @@ function normalize(value, fallback = [0, 0, 1]) {
|
|
|
194
201
|
return [value[0] / length, value[1] / length, value[2] / length];
|
|
195
202
|
}
|
|
196
203
|
|
|
204
|
+
function hashUint32(value) {
|
|
205
|
+
let x = value >>> 0;
|
|
206
|
+
x = ((((x >>> 16) ^ x) >>> 0) * 0x45d9f3b) >>> 0;
|
|
207
|
+
x = ((((x >>> 16) ^ x) >>> 0) * 0x45d9f3b) >>> 0;
|
|
208
|
+
return ((x >>> 16) ^ x) >>> 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
|
|
212
|
+
let x =
|
|
213
|
+
((pixelId >>> 0) * 747796405) ^
|
|
214
|
+
((sampleId >>> 0) * 2891336453) ^
|
|
215
|
+
((bounce >>> 0) * 277803737) ^
|
|
216
|
+
((frameIndex >>> 0) * 1442695041) ^
|
|
217
|
+
((dimension >>> 0) * 1597334677);
|
|
218
|
+
x >>>= 0;
|
|
219
|
+
x ^= x >>> 16;
|
|
220
|
+
x = (x * 0x7feb352d) >>> 0;
|
|
221
|
+
x ^= x >>> 15;
|
|
222
|
+
x = (x * 0x846ca68b) >>> 0;
|
|
223
|
+
x ^= x >>> 16;
|
|
224
|
+
return x >>> 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function random01FromSeed(seed) {
|
|
228
|
+
return (hashUint32(seed) & 0x00ffffff) / 16777215;
|
|
229
|
+
}
|
|
230
|
+
|
|
197
231
|
function getArrayLikeLength(value) {
|
|
198
232
|
return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
|
|
199
233
|
}
|
|
@@ -882,6 +916,36 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
|
|
|
882
916
|
});
|
|
883
917
|
}
|
|
884
918
|
|
|
919
|
+
function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
|
|
920
|
+
void origin;
|
|
921
|
+
const rayDirection = normalize(direction, [0, 1, 0]);
|
|
922
|
+
const upFactor = clamp(rayDirection[1] * 0.5 + 0.5, 0, 1);
|
|
923
|
+
const sunDirection = normalize(
|
|
924
|
+
config.environmentLighting?.sunDirection ?? DEFAULT_ENVIRONMENT_LIGHTING.sunDirection,
|
|
925
|
+
DEFAULT_ENVIRONMENT_LIGHTING.sunDirection
|
|
926
|
+
);
|
|
927
|
+
const sunGlow = Math.pow(clamp(dot(rayDirection, sunDirection), 0, 1), 192);
|
|
928
|
+
const horizonColor =
|
|
929
|
+
config.environmentLighting?.horizonColor ?? DEFAULT_ENVIRONMENT_LIGHTING.horizonColor;
|
|
930
|
+
const zenithColor =
|
|
931
|
+
config.environmentLighting?.zenithColor ?? DEFAULT_ENVIRONMENT_LIGHTING.zenithColor;
|
|
932
|
+
const sunColor = config.environmentLighting?.sunColor ?? DEFAULT_ENVIRONMENT_LIGHTING.sunColor;
|
|
933
|
+
const intensity = Math.max(
|
|
934
|
+
0.0001,
|
|
935
|
+
Number(config.environmentLighting?.intensity ?? DEFAULT_ENVIRONMENT_LIGHTING.intensity)
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
return Object.freeze([
|
|
939
|
+
(horizonColor[0] * (1 - upFactor) + zenithColor[0] * upFactor + sunColor[0] * sunGlow) *
|
|
940
|
+
intensity,
|
|
941
|
+
(horizonColor[1] * (1 - upFactor) + zenithColor[1] * upFactor + sunColor[1] * sunGlow) *
|
|
942
|
+
intensity,
|
|
943
|
+
(horizonColor[2] * (1 - upFactor) + zenithColor[2] * upFactor + sunColor[2] * sunGlow) *
|
|
944
|
+
intensity,
|
|
945
|
+
1,
|
|
946
|
+
]);
|
|
947
|
+
}
|
|
948
|
+
|
|
885
949
|
function resolveEnvironmentPortalMode(value, hasPortals) {
|
|
886
950
|
if (value === undefined || value === null) {
|
|
887
951
|
return hasPortals ? 2 : 0;
|
|
@@ -1099,6 +1163,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
|
|
|
1099
1163
|
environmentPortalBytes,
|
|
1100
1164
|
configBytes: CONFIG_BUFFER_BYTES,
|
|
1101
1165
|
counterBytes: COUNTER_BUFFER_BYTES,
|
|
1166
|
+
indirectDispatchBytes: INDIRECT_DISPATCH_ARGS_BYTES,
|
|
1102
1167
|
totalHotBufferBytes:
|
|
1103
1168
|
queueBytes * 2 +
|
|
1104
1169
|
hitBytes +
|
|
@@ -1110,7 +1175,8 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
|
|
|
1110
1175
|
emissiveTriangleMetadataBytes +
|
|
1111
1176
|
environmentPortalBytes +
|
|
1112
1177
|
CONFIG_BUFFER_BYTES +
|
|
1113
|
-
COUNTER_BUFFER_BYTES
|
|
1178
|
+
COUNTER_BUFFER_BYTES +
|
|
1179
|
+
INDIRECT_DISPATCH_ARGS_BYTES,
|
|
1114
1180
|
});
|
|
1115
1181
|
}
|
|
1116
1182
|
|
|
@@ -1212,6 +1278,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
1212
1278
|
);
|
|
1213
1279
|
|
|
1214
1280
|
return Object.freeze({
|
|
1281
|
+
mode: rendererWavefrontComputeMode,
|
|
1215
1282
|
width,
|
|
1216
1283
|
height,
|
|
1217
1284
|
maxDepth,
|
|
@@ -1272,6 +1339,9 @@ function getGpuUsageConstants() {
|
|
|
1272
1339
|
) {
|
|
1273
1340
|
throw new Error("WebGPU runtime unavailable. Required GPU constants are missing.");
|
|
1274
1341
|
}
|
|
1342
|
+
if (typeof GPUBufferUsage.INDIRECT !== "number") {
|
|
1343
|
+
throw new Error("WebGPU runtime unavailable. GPUBufferUsage.INDIRECT is missing.");
|
|
1344
|
+
}
|
|
1275
1345
|
|
|
1276
1346
|
return {
|
|
1277
1347
|
buffer: GPUBufferUsage,
|
|
@@ -1489,6 +1559,229 @@ function createTiles(width, height, tileSize) {
|
|
|
1489
1559
|
return Object.freeze(tiles);
|
|
1490
1560
|
}
|
|
1491
1561
|
|
|
1562
|
+
function normalizeReferenceTile(config, tileInput = {}) {
|
|
1563
|
+
const tileX = clamp(
|
|
1564
|
+
readNonNegativeInteger("tile.x", tileInput.x, 0),
|
|
1565
|
+
0,
|
|
1566
|
+
Math.max(0, config.width - 1)
|
|
1567
|
+
);
|
|
1568
|
+
const tileY = clamp(
|
|
1569
|
+
readNonNegativeInteger("tile.y", tileInput.y, 0),
|
|
1570
|
+
0,
|
|
1571
|
+
Math.max(0, config.height - 1)
|
|
1572
|
+
);
|
|
1573
|
+
const tileWidth = clamp(
|
|
1574
|
+
readPositiveInteger("tile.width", tileInput.width, config.width - tileX),
|
|
1575
|
+
1,
|
|
1576
|
+
config.width - tileX
|
|
1577
|
+
);
|
|
1578
|
+
const tileHeight = clamp(
|
|
1579
|
+
readPositiveInteger("tile.height", tileInput.height, config.height - tileY),
|
|
1580
|
+
1,
|
|
1581
|
+
config.height - tileY
|
|
1582
|
+
);
|
|
1583
|
+
|
|
1584
|
+
return Object.freeze({
|
|
1585
|
+
x: tileX,
|
|
1586
|
+
y: tileY,
|
|
1587
|
+
width: tileWidth,
|
|
1588
|
+
height: tileHeight,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function repairReferenceShadingNormal(geometricNormal, shadingNormal) {
|
|
1593
|
+
const normal = normalize(shadingNormal, geometricNormal);
|
|
1594
|
+
return dot(normal, geometricNormal) < 0 ? scale(normal, -1) : normal;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function readOptionalMaxDistance(value) {
|
|
1598
|
+
if (value === undefined || value === null) {
|
|
1599
|
+
return Number.POSITIVE_INFINITY;
|
|
1600
|
+
}
|
|
1601
|
+
const numeric = Number(value);
|
|
1602
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
1603
|
+
throw new Error("maxDistance must be a positive finite number when provided.");
|
|
1604
|
+
}
|
|
1605
|
+
return numeric;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
export function createWavefrontReferenceRay(config, options = {}) {
|
|
1609
|
+
if (!config || typeof config !== "object") {
|
|
1610
|
+
throw new Error("config must be a wavefront path tracing config.");
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const tile = normalizeReferenceTile(config, options.tile);
|
|
1614
|
+
const tilePixelCount = tile.width * tile.height;
|
|
1615
|
+
const pixelIndex = readNonNegativeInteger("pixelIndex", options.pixelIndex, 0);
|
|
1616
|
+
if (pixelIndex >= tilePixelCount) {
|
|
1617
|
+
throw new Error(`pixelIndex ${pixelIndex} exceeds tile capacity ${tilePixelCount}.`);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const sampleIndex = readNonNegativeInteger("sampleIndex", options.sampleIndex, 0);
|
|
1621
|
+
const frameIndex = readNonNegativeInteger("frameIndex", options.frameIndex, config.frameIndex ?? 0);
|
|
1622
|
+
const jitterScale = clamp(readFiniteNumber("jitterScale", options.jitterScale, 0.35), 0, 1);
|
|
1623
|
+
const localX = pixelIndex % tile.width;
|
|
1624
|
+
const localY = Math.floor(pixelIndex / tile.width);
|
|
1625
|
+
const pixelX = tile.x + localX;
|
|
1626
|
+
const pixelY = tile.y + localY;
|
|
1627
|
+
const sourcePixelId = pixelY * config.width + pixelX;
|
|
1628
|
+
const jitterX = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 1)) - 0.5;
|
|
1629
|
+
const jitterY = random01FromSeed(mixSeed(sourcePixelId, sampleIndex, 0, frameIndex, 2)) - 0.5;
|
|
1630
|
+
const ndcX = ((pixelX + 0.5 + jitterX * jitterScale) / config.width) * 2 - 1;
|
|
1631
|
+
const ndcY = 1 - ((pixelY + 0.5 + jitterY * jitterScale) / config.height) * 2;
|
|
1632
|
+
const viewX = ndcX * config.camera.tanHalfFovY * config.camera.aspect;
|
|
1633
|
+
const viewY = ndcY * config.camera.tanHalfFovY;
|
|
1634
|
+
const direction = normalize(
|
|
1635
|
+
add(
|
|
1636
|
+
add(config.camera.forward, scale(config.camera.right, viewX)),
|
|
1637
|
+
scale(config.camera.up, viewY)
|
|
1638
|
+
),
|
|
1639
|
+
config.camera.forward
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
return Object.freeze({
|
|
1643
|
+
rayId: pixelIndex,
|
|
1644
|
+
parentRayId: 0xffffffff,
|
|
1645
|
+
sourcePixelId,
|
|
1646
|
+
sampleId: sampleIndex,
|
|
1647
|
+
bounce: 0,
|
|
1648
|
+
mediumRefId: 0,
|
|
1649
|
+
flags: 0,
|
|
1650
|
+
origin: Object.freeze([...config.camera.position]),
|
|
1651
|
+
direction: Object.freeze(direction),
|
|
1652
|
+
throughput: Object.freeze([1, 1, 1, 1]),
|
|
1653
|
+
pixelX,
|
|
1654
|
+
pixelY,
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
export function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
|
|
1659
|
+
if (!ray || typeof ray !== "object") {
|
|
1660
|
+
throw new Error("ray must be a wavefront reference ray.");
|
|
1661
|
+
}
|
|
1662
|
+
if (!triangle || typeof triangle !== "object") {
|
|
1663
|
+
throw new Error("triangle must be a wavefront triangle record.");
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const maxDistance = readOptionalMaxDistance(options.maxDistance);
|
|
1667
|
+
const triangleIndex = readNonNegativeInteger("triangleIndex", options.triangleIndex, 0);
|
|
1668
|
+
const edge1 = subtract(triangle.v1, triangle.v0);
|
|
1669
|
+
const edge2 = subtract(triangle.v2, triangle.v0);
|
|
1670
|
+
const pvec = cross(ray.direction, edge2);
|
|
1671
|
+
const determinant = dot(edge1, pvec);
|
|
1672
|
+
if (Math.abs(determinant) < 0.0000001) {
|
|
1673
|
+
return null;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const invDet = 1 / determinant;
|
|
1677
|
+
const tvec = subtract(ray.origin, triangle.v0);
|
|
1678
|
+
const u = dot(tvec, pvec) * invDet;
|
|
1679
|
+
if (u < 0 || u > 1) {
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const qvec = cross(tvec, edge1);
|
|
1684
|
+
const v = dot(ray.direction, qvec) * invDet;
|
|
1685
|
+
if (v < 0 || u + v > 1) {
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const distance = dot(edge2, qvec) * invDet;
|
|
1690
|
+
if (distance <= 0.001 || distance > maxDistance) {
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
const geometric = normalize(cross(edge1, edge2), [0, 1, 0]);
|
|
1695
|
+
const frontFace = dot(ray.direction, geometric) < 0;
|
|
1696
|
+
const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
|
|
1697
|
+
const w = 1 - u - v;
|
|
1698
|
+
const interpolated = [
|
|
1699
|
+
triangle.n0[0] * w + triangle.n1[0] * u + triangle.n2[0] * v,
|
|
1700
|
+
triangle.n0[1] * w + triangle.n1[1] * u + triangle.n2[1] * v,
|
|
1701
|
+
triangle.n0[2] * w + triangle.n1[2] * u + triangle.n2[2] * v,
|
|
1702
|
+
];
|
|
1703
|
+
const shadingNormal = repairReferenceShadingNormal(orientedGeometric, interpolated);
|
|
1704
|
+
const uv = [
|
|
1705
|
+
triangle.uv0[0] * w + triangle.uv1[0] * u + triangle.uv2[0] * v,
|
|
1706
|
+
triangle.uv0[1] * w + triangle.uv1[1] * u + triangle.uv2[1] * v,
|
|
1707
|
+
];
|
|
1708
|
+
const position = add(ray.origin, scale(ray.direction, distance));
|
|
1709
|
+
|
|
1710
|
+
return Object.freeze({
|
|
1711
|
+
hitType: "surface",
|
|
1712
|
+
rayId: ray.rayId,
|
|
1713
|
+
sourcePixelId: ray.sourcePixelId,
|
|
1714
|
+
distance,
|
|
1715
|
+
entityId: triangle.meshId,
|
|
1716
|
+
instanceId: 0,
|
|
1717
|
+
primitiveId: triangle.triangleId,
|
|
1718
|
+
materialId: triangle.materialKind,
|
|
1719
|
+
materialRefId: triangle.materialRefId,
|
|
1720
|
+
mediumRefId: triangle.mediumRefId,
|
|
1721
|
+
barycentrics: Object.freeze([w, u, v]),
|
|
1722
|
+
uv: Object.freeze(uv),
|
|
1723
|
+
geometricNormal: Object.freeze(orientedGeometric),
|
|
1724
|
+
shadingNormal: Object.freeze(shadingNormal),
|
|
1725
|
+
frontFace,
|
|
1726
|
+
triangleIndex,
|
|
1727
|
+
triangleId: triangle.triangleId,
|
|
1728
|
+
position: Object.freeze(position),
|
|
1729
|
+
color: triangle.color,
|
|
1730
|
+
emission: triangle.emission,
|
|
1731
|
+
material: triangle.material,
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function createWavefrontReferenceEnvironmentHit(config, ray) {
|
|
1736
|
+
const radiance = evaluateReferenceEnvironmentRadiance(config, ray.origin, ray.direction);
|
|
1737
|
+
return Object.freeze({
|
|
1738
|
+
hitType: "environment",
|
|
1739
|
+
rayId: ray.rayId,
|
|
1740
|
+
sourcePixelId: ray.sourcePixelId,
|
|
1741
|
+
distance: -1,
|
|
1742
|
+
entityId: 0,
|
|
1743
|
+
instanceId: 0,
|
|
1744
|
+
primitiveId: 0,
|
|
1745
|
+
materialId: 0,
|
|
1746
|
+
materialRefId: 0,
|
|
1747
|
+
mediumRefId: 0,
|
|
1748
|
+
barycentrics: Object.freeze([0, 0, 0]),
|
|
1749
|
+
uv: Object.freeze([0, 0]),
|
|
1750
|
+
geometricNormal: Object.freeze(scale(ray.direction, -1)),
|
|
1751
|
+
shadingNormal: Object.freeze(scale(ray.direction, -1)),
|
|
1752
|
+
frontFace: true,
|
|
1753
|
+
triangleIndex: -1,
|
|
1754
|
+
triangleId: -1,
|
|
1755
|
+
position: Object.freeze(add(ray.origin, scale(ray.direction, 1000))),
|
|
1756
|
+
color: Object.freeze([0, 0, 0, 0]),
|
|
1757
|
+
emission: radiance,
|
|
1758
|
+
material: Object.freeze([1, 0, 1, 1]),
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
export function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
|
|
1763
|
+
if (!config || typeof config !== "object") {
|
|
1764
|
+
throw new Error("config must be a wavefront path tracing config.");
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
const source = Array.isArray(triangles) ? triangles : [];
|
|
1768
|
+
let nearestHit = null;
|
|
1769
|
+
let nearestDistance = readOptionalMaxDistance(options.maxDistance);
|
|
1770
|
+
|
|
1771
|
+
source.forEach((triangle, index) => {
|
|
1772
|
+
const hit = intersectWavefrontReferenceTriangle(ray, triangle, {
|
|
1773
|
+
maxDistance: Number.isFinite(nearestDistance) ? nearestDistance : undefined,
|
|
1774
|
+
triangleIndex: index,
|
|
1775
|
+
});
|
|
1776
|
+
if (hit && hit.distance < nearestDistance) {
|
|
1777
|
+
nearestDistance = hit.distance;
|
|
1778
|
+
nearestHit = hit;
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
return nearestHit ?? createWavefrontReferenceEnvironmentHit(config, ray);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1492
1785
|
function clampTileSizeForDevice(config, device) {
|
|
1493
1786
|
const limit = Number(device?.limits?.maxStorageBufferBindingSize);
|
|
1494
1787
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
@@ -1738,6 +2031,10 @@ struct Counters {
|
|
|
1738
2031
|
nextCount: atomic<u32>,
|
|
1739
2032
|
terminatedCount: atomic<u32>,
|
|
1740
2033
|
hitCount: atomic<u32>,
|
|
2034
|
+
dispatchX: u32,
|
|
2035
|
+
dispatchY: u32,
|
|
2036
|
+
dispatchZ: u32,
|
|
2037
|
+
dispatchPad: u32,
|
|
1741
2038
|
};
|
|
1742
2039
|
|
|
1743
2040
|
struct Candidate {
|
|
@@ -1900,6 +2197,18 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
|
|
|
1900
2197
|
return environment_radiance(origin, direction);
|
|
1901
2198
|
}
|
|
1902
2199
|
|
|
2200
|
+
fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
|
|
2201
|
+
let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
|
|
2202
|
+
let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
|
|
2203
|
+
let normalEnvironment = gated_environment_radiance(
|
|
2204
|
+
hit.position.xyz + normal * 0.003,
|
|
2205
|
+
normal
|
|
2206
|
+
);
|
|
2207
|
+
let environmentFloor = max(config.ambientColor.xyz, normalEnvironment * 0.12);
|
|
2208
|
+
let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
|
|
2209
|
+
return clamp_sample_radiance(ray.throughput.xyz * surfaceColor * environmentFloor * materialFloor);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
1903
2212
|
fn default_mesh_range() -> MeshRange {
|
|
1904
2213
|
return MeshRange(
|
|
1905
2214
|
0u,
|
|
@@ -2471,6 +2780,17 @@ fn tone_map_radiance(value: vec3<f32>) -> vec3<f32> {
|
|
|
2471
2780
|
return pow(clamp(mapped, vec3<f32>(0.0), vec3<f32>(1.0)), vec3<f32>(1.0 / 2.2));
|
|
2472
2781
|
}
|
|
2473
2782
|
|
|
2783
|
+
fn ray_workgroups_for_count(rayCount: u32) -> u32 {
|
|
2784
|
+
return max(1u, (rayCount + 63u) / 64u);
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
fn write_active_dispatch_args(activeCount: u32) {
|
|
2788
|
+
counters.dispatchX = ray_workgroups_for_count(activeCount);
|
|
2789
|
+
counters.dispatchY = 1u;
|
|
2790
|
+
counters.dispatchZ = 1u;
|
|
2791
|
+
counters.dispatchPad = 0u;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2474
2794
|
fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
|
|
2475
2795
|
return value / (vec3<f32>(1.0) + value);
|
|
2476
2796
|
}
|
|
@@ -2483,6 +2803,7 @@ fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
|
2483
2803
|
atomicStore(&counters.nextCount, 0u);
|
|
2484
2804
|
atomicStore(&counters.terminatedCount, 0u);
|
|
2485
2805
|
atomicStore(&counters.hitCount, 0u);
|
|
2806
|
+
write_active_dispatch_args(config.tilePixelCount);
|
|
2486
2807
|
}
|
|
2487
2808
|
if (index >= config.tilePixelCount) {
|
|
2488
2809
|
return;
|
|
@@ -2758,9 +3079,9 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
|
2758
3079
|
}
|
|
2759
3080
|
|
|
2760
3081
|
if (ray.bounce + 1u >= config.maxDepth) {
|
|
3082
|
+
let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
|
|
2761
3083
|
accumulation[ray.rayId] =
|
|
2762
|
-
accumulation[ray.rayId] +
|
|
2763
|
-
vec4<f32>(ray.throughput.xyz * config.ambientColor.xyz * sample_weight(), 1.0);
|
|
3084
|
+
accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
|
|
2764
3085
|
atomicAdd(&counters.terminatedCount, 1u);
|
|
2765
3086
|
return;
|
|
2766
3087
|
}
|
|
@@ -2769,6 +3090,10 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
|
2769
3090
|
let scatter = scatter_direction(ray, hit, seed);
|
|
2770
3091
|
let nextIndex = atomicAdd(&counters.nextCount, 1u);
|
|
2771
3092
|
if (nextIndex >= config.tilePixelCount) {
|
|
3093
|
+
let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
|
|
3094
|
+
accumulation[ray.rayId] =
|
|
3095
|
+
accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
|
|
3096
|
+
atomicAdd(&counters.terminatedCount, 1u);
|
|
2772
3097
|
return;
|
|
2773
3098
|
}
|
|
2774
3099
|
let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
@@ -2797,8 +3122,10 @@ fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
|
2797
3122
|
return;
|
|
2798
3123
|
}
|
|
2799
3124
|
let nextCount = atomicLoad(&counters.nextCount);
|
|
2800
|
-
|
|
3125
|
+
let activeCount = min(nextCount, config.tilePixelCount);
|
|
3126
|
+
atomicStore(&counters.activeCount, activeCount);
|
|
2801
3127
|
atomicStore(&counters.nextCount, 0u);
|
|
3128
|
+
write_active_dispatch_args(activeCount);
|
|
2802
3129
|
}
|
|
2803
3130
|
|
|
2804
3131
|
@compute @workgroup_size(64)
|
|
@@ -3093,10 +3420,16 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3093
3420
|
);
|
|
3094
3421
|
const counterBuffer = createBuffer(
|
|
3095
3422
|
device,
|
|
3096
|
-
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
3423
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST | constants.buffer.COPY_SRC,
|
|
3097
3424
|
COUNTER_BUFFER_BYTES,
|
|
3098
3425
|
"plasius.wavefront.counters"
|
|
3099
3426
|
);
|
|
3427
|
+
const activeDispatchBuffer = createBuffer(
|
|
3428
|
+
device,
|
|
3429
|
+
constants.buffer.INDIRECT | constants.buffer.COPY_DST,
|
|
3430
|
+
INDIRECT_DISPATCH_ARGS_BYTES,
|
|
3431
|
+
"plasius.wavefront.activeDispatchArgs"
|
|
3432
|
+
);
|
|
3100
3433
|
|
|
3101
3434
|
let packedScene = packWavefrontSceneObjects(config.sceneObjects, config.sceneObjectCapacity);
|
|
3102
3435
|
device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
|
|
@@ -3538,27 +3871,36 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3538
3871
|
}
|
|
3539
3872
|
|
|
3540
3873
|
function encodeTileSample(encoder, tile, configOffset) {
|
|
3541
|
-
const
|
|
3542
|
-
label: "plasius.wavefront.
|
|
3874
|
+
const generatePass = encoder.beginComputePass({
|
|
3875
|
+
label: "plasius.wavefront.generatePrimaryRaysPass",
|
|
3543
3876
|
});
|
|
3544
3877
|
const tileWorkgroups = Math.ceil((tile.width * tile.height) / WORKGROUP_SIZE);
|
|
3545
|
-
const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
|
|
3546
3878
|
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3879
|
+
generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
|
|
3880
|
+
generatePass.setPipeline(pipelines.generatePrimaryRays);
|
|
3881
|
+
generatePass.dispatchWorkgroups(tileWorkgroups);
|
|
3882
|
+
generatePass.end();
|
|
3550
3883
|
|
|
3551
3884
|
for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
|
|
3885
|
+
encoder.copyBufferToBuffer(
|
|
3886
|
+
counterBuffer,
|
|
3887
|
+
COUNTER_DISPATCH_ARGS_OFFSET,
|
|
3888
|
+
activeDispatchBuffer,
|
|
3889
|
+
0,
|
|
3890
|
+
INDIRECT_DISPATCH_ARGS_BYTES
|
|
3891
|
+
);
|
|
3892
|
+
const passEncoder = encoder.beginComputePass({
|
|
3893
|
+
label: `plasius.wavefront.bounce.${bounceIndex}`,
|
|
3894
|
+
});
|
|
3552
3895
|
passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
|
|
3553
3896
|
passEncoder.setPipeline(pipelines.intersectActiveQueue);
|
|
3554
|
-
passEncoder.
|
|
3897
|
+
passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
|
|
3555
3898
|
passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
|
|
3556
|
-
passEncoder.
|
|
3899
|
+
passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
|
|
3557
3900
|
passEncoder.setPipeline(pipelines.compactAndSwapQueues);
|
|
3558
3901
|
passEncoder.dispatchWorkgroups(1);
|
|
3902
|
+
passEncoder.end();
|
|
3559
3903
|
}
|
|
3560
|
-
|
|
3561
|
-
passEncoder.end();
|
|
3562
3904
|
}
|
|
3563
3905
|
|
|
3564
3906
|
function encodeTileOutput(encoder, tile, configOffset) {
|
|
@@ -3710,6 +4052,32 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3710
4052
|
});
|
|
3711
4053
|
}
|
|
3712
4054
|
|
|
4055
|
+
async function renderFrame(renderOptions = {}) {
|
|
4056
|
+
const frameStats = renderOnce();
|
|
4057
|
+
const probe =
|
|
4058
|
+
renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
|
|
4059
|
+
const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
|
|
4060
|
+
return Object.freeze({
|
|
4061
|
+
...frameStats,
|
|
4062
|
+
outputProbe: probe
|
|
4063
|
+
? Object.freeze({
|
|
4064
|
+
...probe,
|
|
4065
|
+
sampledPixels: 1,
|
|
4066
|
+
nonZeroSamples: maxChannel > 0 ? 1 : 0,
|
|
4067
|
+
maxChannel,
|
|
4068
|
+
})
|
|
4069
|
+
: null,
|
|
4070
|
+
bounces: [],
|
|
4071
|
+
termination: Object.freeze({
|
|
4072
|
+
emissive: 0,
|
|
4073
|
+
environment: 0,
|
|
4074
|
+
ambientFallback: 0,
|
|
4075
|
+
maxDepth: 0,
|
|
4076
|
+
}),
|
|
4077
|
+
queueOverflow: 0,
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
|
|
3713
4081
|
function updateSceneObjects(sceneObjects) {
|
|
3714
4082
|
const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
|
|
3715
4083
|
packedScene = nextPackedScene;
|
|
@@ -3770,6 +4138,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3770
4138
|
configBuffer.destroy?.();
|
|
3771
4139
|
bvhBuildConfigBuffer.destroy?.();
|
|
3772
4140
|
counterBuffer.destroy?.();
|
|
4141
|
+
activeDispatchBuffer.destroy?.();
|
|
3773
4142
|
radianceTexture.destroy?.();
|
|
3774
4143
|
denoiseScratchTexture.destroy?.();
|
|
3775
4144
|
outputTexture.destroy?.();
|
|
@@ -3783,9 +4152,31 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3783
4152
|
format,
|
|
3784
4153
|
config,
|
|
3785
4154
|
renderOnce,
|
|
4155
|
+
renderFrame,
|
|
3786
4156
|
readOutputProbe,
|
|
3787
4157
|
updateSceneObjects,
|
|
3788
4158
|
getSnapshot,
|
|
3789
4159
|
destroy,
|
|
3790
4160
|
});
|
|
3791
4161
|
}
|
|
4162
|
+
|
|
4163
|
+
export async function renderWavefrontPathTracingComputeFrame(options = {}) {
|
|
4164
|
+
const renderer = await createWavefrontPathTracingComputeRenderer(options);
|
|
4165
|
+
try {
|
|
4166
|
+
return await renderer.renderFrame(options);
|
|
4167
|
+
} finally {
|
|
4168
|
+
renderer.destroy();
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
export function createWavefrontPathTracingComputeShaderSource(options = {}) {
|
|
4173
|
+
const workgroupSize = readPositiveInteger(
|
|
4174
|
+
"workgroupSize",
|
|
4175
|
+
options.workgroupSize ?? rendererWavefrontComputeWorkgroupSize,
|
|
4176
|
+
rendererWavefrontComputeWorkgroupSize
|
|
4177
|
+
);
|
|
4178
|
+
if (workgroupSize !== rendererWavefrontComputeWorkgroupSize) {
|
|
4179
|
+
throw new Error(`wavefront mesh compute currently requires workgroupSize=${rendererWavefrontComputeWorkgroupSize}.`);
|
|
4180
|
+
}
|
|
4181
|
+
return WAVEFRONT_COMPUTE_WGSL;
|
|
4182
|
+
}
|