@plasius/gpu-renderer 0.1.14 → 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 +28 -0
- package/README.md +21 -0
- package/dist/index.cjs +643 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +640 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.d.ts +144 -1
- package/src/index.js +3 -0
- package/src/wavefront-compute.js +684 -73
package/src/wavefront-compute.js
CHANGED
|
@@ -4,6 +4,7 @@ const DEFAULT_MAX_DEPTH = 6;
|
|
|
4
4
|
const DEFAULT_TILE_SIZE = 128;
|
|
5
5
|
const DEFAULT_SAMPLES_PER_PIXEL = 1;
|
|
6
6
|
const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
|
|
7
|
+
const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
|
|
7
8
|
const WORKGROUP_SIZE = 64;
|
|
8
9
|
const RAY_RECORD_BYTES = 80;
|
|
9
10
|
const HIT_RECORD_BYTES = 208;
|
|
@@ -14,9 +15,11 @@ const TRIANGLE_RECORD_BYTES = 208;
|
|
|
14
15
|
const BVH_NODE_RECORD_BYTES = 48;
|
|
15
16
|
const BVH_LEAF_REF_RECORD_BYTES = 16;
|
|
16
17
|
const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
|
|
18
|
+
const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
|
|
17
19
|
const ACCUMULATION_RECORD_BYTES = 16;
|
|
18
|
-
const CONFIG_BUFFER_BYTES =
|
|
20
|
+
const CONFIG_BUFFER_BYTES = 272;
|
|
19
21
|
const COUNTER_BUFFER_BYTES = 16;
|
|
22
|
+
const TRACE_STORAGE_BUFFER_BINDINGS = 9;
|
|
20
23
|
const HIT_TYPE_SURFACE = 0;
|
|
21
24
|
const HIT_TYPE_EMISSIVE = 1;
|
|
22
25
|
const MATERIAL_DIFFUSE = 0;
|
|
@@ -48,6 +51,7 @@ const DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
|
|
|
48
51
|
|
|
49
52
|
export const wavefrontPathTracingComputeLimits = Object.freeze({
|
|
50
53
|
workgroupSize: WORKGROUP_SIZE,
|
|
54
|
+
traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
|
|
51
55
|
rayRecordBytes: RAY_RECORD_BYTES,
|
|
52
56
|
hitRecordBytes: HIT_RECORD_BYTES,
|
|
53
57
|
sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
|
|
@@ -58,6 +62,7 @@ export const wavefrontPathTracingComputeLimits = Object.freeze({
|
|
|
58
62
|
bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
|
|
59
63
|
emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
|
|
60
64
|
emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
|
|
65
|
+
environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
|
|
61
66
|
accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
|
|
62
67
|
});
|
|
63
68
|
|
|
@@ -189,6 +194,33 @@ function normalize(value, fallback = [0, 0, 1]) {
|
|
|
189
194
|
return [value[0] / length, value[1] / length, value[2] / length];
|
|
190
195
|
}
|
|
191
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
|
+
|
|
192
224
|
function getArrayLikeLength(value) {
|
|
193
225
|
return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
|
|
194
226
|
}
|
|
@@ -877,6 +909,165 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
|
|
|
877
909
|
});
|
|
878
910
|
}
|
|
879
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
|
+
|
|
942
|
+
function resolveEnvironmentPortalMode(value, hasPortals) {
|
|
943
|
+
if (value === undefined || value === null) {
|
|
944
|
+
return hasPortals ? 2 : 0;
|
|
945
|
+
}
|
|
946
|
+
if (Number.isInteger(value) && value >= 0 && value <= 2) {
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
949
|
+
if (value === "disabled") {
|
|
950
|
+
return 0;
|
|
951
|
+
}
|
|
952
|
+
if (value === "guide") {
|
|
953
|
+
return 1;
|
|
954
|
+
}
|
|
955
|
+
if (value === "guide-and-gate" || value === "gate") {
|
|
956
|
+
return 2;
|
|
957
|
+
}
|
|
958
|
+
throw new Error(
|
|
959
|
+
"environmentPortalMode must be disabled, guide, guide-and-gate, or an integer between 0 and 2."
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function orthogonalPortalTangent(normal) {
|
|
964
|
+
if (Math.abs(normal[1]) < 0.92) {
|
|
965
|
+
return normalize(cross([0, 1, 0], normal), [1, 0, 0]);
|
|
966
|
+
}
|
|
967
|
+
return normalize(cross([1, 0, 0], normal), [0, 0, 1]);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function resolvePortalTangent(value, normal) {
|
|
971
|
+
const fallback = orthogonalPortalTangent(normal);
|
|
972
|
+
const tangent = asUnitVec3(value, fallback);
|
|
973
|
+
const projected = subtract(tangent, scale(normal, dot(tangent, normal)));
|
|
974
|
+
return normalize(projected, fallback);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function readPositiveFiniteNumber(name, value, fallback) {
|
|
978
|
+
const numeric = readFiniteNumber(name, value, fallback);
|
|
979
|
+
if (numeric <= 0) {
|
|
980
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
981
|
+
}
|
|
982
|
+
return numeric;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function readPortalExtent(name, value, halfName, halfValue) {
|
|
986
|
+
if (value !== undefined && value !== null) {
|
|
987
|
+
return readPositiveFiniteNumber(name, value, 1);
|
|
988
|
+
}
|
|
989
|
+
return readPositiveFiniteNumber(halfName, halfValue, 0.5) * 2;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function normalizeEnvironmentPortal(portal, index) {
|
|
993
|
+
if (!portal || typeof portal !== "object") {
|
|
994
|
+
throw new Error(`environmentPortals[${index}] must be an object.`);
|
|
995
|
+
}
|
|
996
|
+
const shape = portal.shape ?? portal.kind ?? "rectangle";
|
|
997
|
+
if (shape !== "rectangle") {
|
|
998
|
+
throw new Error(`environmentPortals[${index}].shape must be "rectangle".`);
|
|
999
|
+
}
|
|
1000
|
+
const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
|
|
1001
|
+
const normal = asUnitVec3(portal.normal, [0, 0, 1]);
|
|
1002
|
+
const tangent = resolvePortalTangent(portal.tangent, normal);
|
|
1003
|
+
const bitangent = normalize(cross(normal, tangent), [0, 1, 0]);
|
|
1004
|
+
const width = readPortalExtent(
|
|
1005
|
+
`environmentPortals[${index}].width`,
|
|
1006
|
+
portal.width,
|
|
1007
|
+
`environmentPortals[${index}].halfWidth`,
|
|
1008
|
+
portal.halfWidth
|
|
1009
|
+
);
|
|
1010
|
+
const height = readPortalExtent(
|
|
1011
|
+
`environmentPortals[${index}].height`,
|
|
1012
|
+
portal.height,
|
|
1013
|
+
`environmentPortals[${index}].halfHeight`,
|
|
1014
|
+
portal.halfHeight
|
|
1015
|
+
);
|
|
1016
|
+
const radianceScale = Math.max(
|
|
1017
|
+
0,
|
|
1018
|
+
readFiniteNumber(
|
|
1019
|
+
`environmentPortals[${index}].radianceScale`,
|
|
1020
|
+
portal.radianceScale ?? portal.intensity,
|
|
1021
|
+
1
|
|
1022
|
+
)
|
|
1023
|
+
);
|
|
1024
|
+
return Object.freeze({
|
|
1025
|
+
kind: 1,
|
|
1026
|
+
flags: portal.twoSided === false ? 0 : 1,
|
|
1027
|
+
position: Object.freeze([position[0], position[1], position[2], width * height]),
|
|
1028
|
+
normal: Object.freeze([normal[0], normal[1], normal[2], radianceScale]),
|
|
1029
|
+
tangent: Object.freeze([tangent[0], tangent[1], tangent[2], width * 0.5]),
|
|
1030
|
+
bitangent: Object.freeze([bitangent[0], bitangent[1], bitangent[2], height * 0.5]),
|
|
1031
|
+
color: Object.freeze(asColor(portal.color, [1, 1, 1, 1])),
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function normalizeEnvironmentPortals(value) {
|
|
1036
|
+
if (value === undefined || value === null) {
|
|
1037
|
+
return Object.freeze([]);
|
|
1038
|
+
}
|
|
1039
|
+
if (!Array.isArray(value)) {
|
|
1040
|
+
throw new Error("environmentPortals must be an array when provided.");
|
|
1041
|
+
}
|
|
1042
|
+
return Object.freeze(value.map(normalizeEnvironmentPortal));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function packEnvironmentPortals(portals, capacity) {
|
|
1046
|
+
const bytes = new ArrayBuffer(capacity * ENVIRONMENT_PORTAL_RECORD_BYTES);
|
|
1047
|
+
const data = new DataView(bytes);
|
|
1048
|
+
const floatView = new Float32Array(bytes);
|
|
1049
|
+
portals.forEach((portal, index) => {
|
|
1050
|
+
const byteOffset = index * ENVIRONMENT_PORTAL_RECORD_BYTES;
|
|
1051
|
+
const floatOffset = byteOffset / Float32Array.BYTES_PER_ELEMENT;
|
|
1052
|
+
data.setUint32(byteOffset, portal.kind, true);
|
|
1053
|
+
data.setUint32(byteOffset + 4, portal.flags, true);
|
|
1054
|
+
data.setUint32(byteOffset + 8, 0, true);
|
|
1055
|
+
data.setUint32(byteOffset + 12, 0, true);
|
|
1056
|
+
writeVec4(floatView, floatOffset + 4, portal.position);
|
|
1057
|
+
writeVec4(floatView, floatOffset + 8, portal.normal);
|
|
1058
|
+
writeVec4(floatView, floatOffset + 12, portal.tangent);
|
|
1059
|
+
writeVec4(floatView, floatOffset + 16, portal.bitangent);
|
|
1060
|
+
writeVec4(floatView, floatOffset + 20, portal.color);
|
|
1061
|
+
});
|
|
1062
|
+
return Object.freeze({
|
|
1063
|
+
buffer: bytes,
|
|
1064
|
+
portals,
|
|
1065
|
+
count: portals.length,
|
|
1066
|
+
capacity,
|
|
1067
|
+
recordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
|
|
880
1071
|
function getCanvasDimension(canvas, key, fallback) {
|
|
881
1072
|
const value = Number(canvas?.[key]);
|
|
882
1073
|
if (Number.isFinite(value) && value > 0) {
|
|
@@ -935,6 +1126,11 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
|
|
|
935
1126
|
options.emissiveTriangleCapacity,
|
|
936
1127
|
0
|
|
937
1128
|
);
|
|
1129
|
+
const environmentPortalCapacity = readNonNegativeInteger(
|
|
1130
|
+
"environmentPortalCapacity",
|
|
1131
|
+
options.environmentPortalCapacity,
|
|
1132
|
+
0
|
|
1133
|
+
);
|
|
938
1134
|
const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
|
|
939
1135
|
const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
|
|
940
1136
|
const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
|
|
@@ -944,6 +1140,8 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
|
|
|
944
1140
|
const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
|
|
945
1141
|
const emissiveTriangleMetadataBytes =
|
|
946
1142
|
emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
|
|
1143
|
+
const environmentPortalBytes =
|
|
1144
|
+
environmentPortalCapacity * ENVIRONMENT_PORTAL_RECORD_BYTES;
|
|
947
1145
|
|
|
948
1146
|
return Object.freeze({
|
|
949
1147
|
queueBytes,
|
|
@@ -955,6 +1153,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
|
|
|
955
1153
|
bvhNodeBytes,
|
|
956
1154
|
bvhLeafReferenceBytes,
|
|
957
1155
|
emissiveTriangleMetadataBytes,
|
|
1156
|
+
environmentPortalBytes,
|
|
958
1157
|
configBytes: CONFIG_BUFFER_BYTES,
|
|
959
1158
|
counterBytes: COUNTER_BUFFER_BYTES,
|
|
960
1159
|
totalHotBufferBytes:
|
|
@@ -966,6 +1165,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
|
|
|
966
1165
|
bvhNodeBytes +
|
|
967
1166
|
bvhLeafReferenceBytes +
|
|
968
1167
|
emissiveTriangleMetadataBytes +
|
|
1168
|
+
environmentPortalBytes +
|
|
969
1169
|
CONFIG_BUFFER_BYTES +
|
|
970
1170
|
COUNTER_BUFFER_BYTES,
|
|
971
1171
|
});
|
|
@@ -1048,6 +1248,25 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
1048
1248
|
environmentColor,
|
|
1049
1249
|
ambientColor
|
|
1050
1250
|
);
|
|
1251
|
+
const environmentPortals = normalizeEnvironmentPortals(
|
|
1252
|
+
options.environmentPortals ??
|
|
1253
|
+
options.environmentLightPortals ??
|
|
1254
|
+
options.environmentLighting?.environmentPortals
|
|
1255
|
+
);
|
|
1256
|
+
const environmentPortalCapacity = Math.max(
|
|
1257
|
+
environmentPortals.length,
|
|
1258
|
+
readNonNegativeInteger(
|
|
1259
|
+
"environmentPortalCapacity",
|
|
1260
|
+
options.environmentPortalCapacity,
|
|
1261
|
+
DEFAULT_ENVIRONMENT_PORTAL_CAPACITY
|
|
1262
|
+
)
|
|
1263
|
+
);
|
|
1264
|
+
const environmentPortalMode = resolveEnvironmentPortalMode(
|
|
1265
|
+
options.environmentPortalMode ??
|
|
1266
|
+
options.portalMode ??
|
|
1267
|
+
options.environmentLighting?.environmentPortalMode,
|
|
1268
|
+
environmentPortals.length > 0
|
|
1269
|
+
);
|
|
1051
1270
|
|
|
1052
1271
|
return Object.freeze({
|
|
1053
1272
|
width,
|
|
@@ -1077,6 +1296,10 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
1077
1296
|
environmentColor: environmentLighting.environmentColor,
|
|
1078
1297
|
ambientColor: environmentLighting.ambientColor,
|
|
1079
1298
|
environmentLighting,
|
|
1299
|
+
environmentPortals,
|
|
1300
|
+
environmentPortalCount: environmentPortals.length,
|
|
1301
|
+
environmentPortalCapacity,
|
|
1302
|
+
environmentPortalMode,
|
|
1080
1303
|
displayQuality: options.displayQuality === true,
|
|
1081
1304
|
requiresMeshBvhForDisplayQuality: true,
|
|
1082
1305
|
denoise: options.denoise !== false,
|
|
@@ -1088,6 +1311,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
1088
1311
|
bvhNodeCapacity,
|
|
1089
1312
|
bvhLeafSortCapacity,
|
|
1090
1313
|
emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
|
|
1314
|
+
environmentPortalCapacity,
|
|
1091
1315
|
}),
|
|
1092
1316
|
});
|
|
1093
1317
|
}
|
|
@@ -1298,6 +1522,10 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
|
|
|
1298
1522
|
data.setUint32(244, buildRange.count ?? 0, true);
|
|
1299
1523
|
data.setUint32(248, buildRange.sortItemCount ?? 0, true);
|
|
1300
1524
|
data.setUint32(252, config.emissiveTriangleCount ?? 0, true);
|
|
1525
|
+
data.setUint32(256, config.environmentPortalCount ?? 0, true);
|
|
1526
|
+
data.setUint32(260, config.environmentPortalMode ?? 0, true);
|
|
1527
|
+
data.setUint32(264, 0, true);
|
|
1528
|
+
data.setUint32(268, 0, true);
|
|
1301
1529
|
return bytes;
|
|
1302
1530
|
}
|
|
1303
1531
|
|
|
@@ -1318,6 +1546,229 @@ function createTiles(width, height, tileSize) {
|
|
|
1318
1546
|
return Object.freeze(tiles);
|
|
1319
1547
|
}
|
|
1320
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
|
+
|
|
1321
1772
|
function clampTileSizeForDevice(config, device) {
|
|
1322
1773
|
const limit = Number(device?.limits?.maxStorageBufferBindingSize);
|
|
1323
1774
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
@@ -1556,6 +2007,10 @@ struct FrameConfig {
|
|
|
1556
2007
|
bvhBuildNodeCount: u32,
|
|
1557
2008
|
bvhSortItemCount: u32,
|
|
1558
2009
|
emissiveTriangleCount: u32,
|
|
2010
|
+
environmentPortalCount: u32,
|
|
2011
|
+
environmentPortalMode: u32,
|
|
2012
|
+
_portalPad0: u32,
|
|
2013
|
+
_portalPad1: u32,
|
|
1559
2014
|
};
|
|
1560
2015
|
|
|
1561
2016
|
struct Counters {
|
|
@@ -1579,6 +2034,18 @@ struct Candidate {
|
|
|
1579
2034
|
mediumRefId: u32,
|
|
1580
2035
|
};
|
|
1581
2036
|
|
|
2037
|
+
struct EnvironmentPortal {
|
|
2038
|
+
kind: u32,
|
|
2039
|
+
flags: u32,
|
|
2040
|
+
_pad0: u32,
|
|
2041
|
+
_pad1: u32,
|
|
2042
|
+
position: vec4<f32>,
|
|
2043
|
+
normal: vec4<f32>,
|
|
2044
|
+
tangent: vec4<f32>,
|
|
2045
|
+
bitangent: vec4<f32>,
|
|
2046
|
+
color: vec4<f32>,
|
|
2047
|
+
};
|
|
2048
|
+
|
|
1582
2049
|
@group(0) @binding(0) var<storage, read_write> activeQueue: array<RayRecord>;
|
|
1583
2050
|
@group(0) @binding(1) var<storage, read_write> nextQueue: array<RayRecord>;
|
|
1584
2051
|
@group(0) @binding(2) var<storage, read_write> hits: array<HitRecord>;
|
|
@@ -1598,6 +2065,7 @@ struct Candidate {
|
|
|
1598
2065
|
@group(0) @binding(16) var radianceImage: texture_storage_2d<rgba16float, write>;
|
|
1599
2066
|
@group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
|
|
1600
2067
|
@group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
|
|
2068
|
+
@group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
|
|
1601
2069
|
|
|
1602
2070
|
fn hash_u32(value: u32) -> u32 {
|
|
1603
2071
|
var x = value;
|
|
@@ -1638,7 +2106,48 @@ fn saturate(value: f32) -> f32 {
|
|
|
1638
2106
|
return clamp(value, 0.0, 1.0);
|
|
1639
2107
|
}
|
|
1640
2108
|
|
|
1641
|
-
fn
|
|
2109
|
+
fn max_component(value: vec3<f32>) -> f32 {
|
|
2110
|
+
return max(max(value.x, value.y), value.z);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
|
|
2114
|
+
if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
|
|
2115
|
+
return vec3<f32>(1.0);
|
|
2116
|
+
}
|
|
2117
|
+
var scale = vec3<f32>(0.0);
|
|
2118
|
+
for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
|
|
2119
|
+
let portal = environmentPortals[portalIndex];
|
|
2120
|
+
if (portal.kind == 1u) {
|
|
2121
|
+
let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
|
|
2122
|
+
let denominator = dot(direction, portalNormal);
|
|
2123
|
+
let twoSided = (portal.flags & 1u) != 0u;
|
|
2124
|
+
var facing = abs(denominator) > 0.0001;
|
|
2125
|
+
if (!twoSided && denominator <= 0.0001) {
|
|
2126
|
+
facing = false;
|
|
2127
|
+
}
|
|
2128
|
+
if (facing) {
|
|
2129
|
+
let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
|
|
2130
|
+
if (distance > 0.001) {
|
|
2131
|
+
let hitPosition = origin + direction * distance;
|
|
2132
|
+
let local = hitPosition - portal.position.xyz;
|
|
2133
|
+
let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
|
|
2134
|
+
let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
|
|
2135
|
+
let u = dot(local, tangent);
|
|
2136
|
+
let v = dot(local, bitangent);
|
|
2137
|
+
if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
|
|
2138
|
+
let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
|
|
2139
|
+
let angleWeight = max(abs(denominator), 0.08);
|
|
2140
|
+
let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
|
|
2141
|
+
scale = max(scale, portalScale);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return scale;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
|
|
1642
2151
|
let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
|
|
1643
2152
|
let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
|
|
1644
2153
|
let sunDirection = safe_normalize(
|
|
@@ -1649,10 +2158,26 @@ fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
|
|
|
1649
2158
|
let gradient =
|
|
1650
2159
|
config.environmentHorizonColor.xyz * (1.0 - upFactor) +
|
|
1651
2160
|
config.environmentZenithColor.xyz * upFactor;
|
|
2161
|
+
let portalScale = environment_portal_radiance_scale(origin, rayDirection);
|
|
2162
|
+
let portalHit = max_component(portalScale) > 0.0001;
|
|
1652
2163
|
return (
|
|
1653
2164
|
gradient +
|
|
1654
2165
|
config.environmentSunColor.xyz * sunGlow
|
|
1655
|
-
) *
|
|
2166
|
+
) *
|
|
2167
|
+
max(config.environmentSunDirectionIntensity.w, 0.0001) *
|
|
2168
|
+
select(vec3<f32>(1.0), portalScale, portalHit);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
|
|
2172
|
+
let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
|
|
2173
|
+
if (
|
|
2174
|
+
config.environmentPortalCount > 0u &&
|
|
2175
|
+
config.environmentPortalMode == 2u &&
|
|
2176
|
+
max_component(portalScale) <= 0.0001
|
|
2177
|
+
) {
|
|
2178
|
+
return config.ambientColor.xyz * 0.65;
|
|
2179
|
+
}
|
|
2180
|
+
return environment_radiance(origin, direction);
|
|
1656
2181
|
}
|
|
1657
2182
|
|
|
1658
2183
|
fn default_mesh_range() -> MeshRange {
|
|
@@ -1923,7 +2448,7 @@ fn make_ray(pixelIndex: u32) -> RayRecord {
|
|
|
1923
2448
|
}
|
|
1924
2449
|
|
|
1925
2450
|
fn make_miss(ray: RayRecord) -> HitRecord {
|
|
1926
|
-
let radiance =
|
|
2451
|
+
let radiance = gated_environment_radiance(ray.origin.xyz, ray.direction.xyz);
|
|
1927
2452
|
return HitRecord(
|
|
1928
2453
|
ray.rayId,
|
|
1929
2454
|
ray.sourcePixelId,
|
|
@@ -2409,6 +2934,21 @@ fn sample_emissive_triangle_direction(hit: HitRecord, seed: u32, fallback: vec3<
|
|
|
2409
2934
|
return safe_normalize(lightPoint - hit.position.xyz, fallback);
|
|
2410
2935
|
}
|
|
2411
2936
|
|
|
2937
|
+
fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
|
|
2938
|
+
if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
|
|
2939
|
+
return fallback;
|
|
2940
|
+
}
|
|
2941
|
+
let portalSlot = min(
|
|
2942
|
+
u32(random01(seed + 211u) * f32(config.environmentPortalCount)),
|
|
2943
|
+
config.environmentPortalCount - 1u
|
|
2944
|
+
);
|
|
2945
|
+
let portal = environmentPortals[portalSlot];
|
|
2946
|
+
let u = (random01(seed + 223u) * 2.0 - 1.0) * portal.tangent.w;
|
|
2947
|
+
let v = (random01(seed + 227u) * 2.0 - 1.0) * portal.bitangent.w;
|
|
2948
|
+
let portalTarget = portal.position.xyz + portal.tangent.xyz * u + portal.bitangent.xyz * v;
|
|
2949
|
+
return safe_normalize(portalTarget - hit.position.xyz, fallback);
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2412
2952
|
fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
|
|
2413
2953
|
let roughness = clamp(hit.material.x, 0.0, 1.0);
|
|
2414
2954
|
if (hit.materialKind == 1u) {
|
|
@@ -2448,8 +2988,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
|
|
|
2448
2988
|
let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
|
|
2449
2989
|
let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
|
|
2450
2990
|
let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
|
|
2991
|
+
let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
|
|
2992
|
+
let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
|
|
2993
|
+
let useGuidedPortal =
|
|
2994
|
+
!useGuidedLight &&
|
|
2995
|
+
canSamplePortal &&
|
|
2996
|
+
config.environmentPortalCount > 0u &&
|
|
2997
|
+
config.environmentPortalMode > 0u &&
|
|
2998
|
+
random01(seed + 89u) < 0.58;
|
|
2999
|
+
let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
|
|
2451
3000
|
return ScatterResult(
|
|
2452
|
-
vec4<f32>(select(
|
|
3001
|
+
vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
|
|
2453
3002
|
select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
|
|
2454
3003
|
0u,
|
|
2455
3004
|
0u,
|
|
@@ -2658,6 +3207,32 @@ fn fragmentMain(in: VertexOut) -> @location(0) vec4<f32> {
|
|
|
2658
3207
|
}
|
|
2659
3208
|
`;
|
|
2660
3209
|
|
|
3210
|
+
function createWavefrontDeviceDescriptor(adapter, options = {}) {
|
|
3211
|
+
const requiredLimits = { ...(options.requiredLimits ?? {}) };
|
|
3212
|
+
const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
|
|
3213
|
+
if (Number.isFinite(exposedStorageBufferLimit)) {
|
|
3214
|
+
if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
|
|
3215
|
+
throw new Error(
|
|
3216
|
+
`Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, ` +
|
|
3217
|
+
`but this adapter exposes ${exposedStorageBufferLimit}.`
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
|
|
3221
|
+
Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
|
|
3222
|
+
TRACE_STORAGE_BUFFER_BINDINGS
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
const descriptor = { ...(options.deviceDescriptor ?? {}) };
|
|
3227
|
+
if (Object.keys(requiredLimits).length > 0) {
|
|
3228
|
+
descriptor.requiredLimits = {
|
|
3229
|
+
...(descriptor.requiredLimits ?? {}),
|
|
3230
|
+
...requiredLimits,
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
return Object.keys(descriptor).length > 0 ? descriptor : undefined;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
2661
3236
|
export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
2662
3237
|
assertAnalyticDisplayQualityPolicy(options);
|
|
2663
3238
|
const constants = getGpuUsageConstants();
|
|
@@ -2678,7 +3253,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
2678
3253
|
throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
|
|
2679
3254
|
}
|
|
2680
3255
|
|
|
2681
|
-
const device = await adapter.requestDevice();
|
|
3256
|
+
const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
|
|
2682
3257
|
const context = canvas.getContext("webgpu");
|
|
2683
3258
|
if (!context || typeof context.configure !== "function") {
|
|
2684
3259
|
throw new Error("Canvas WebGPU context does not support configure().");
|
|
@@ -2758,18 +3333,19 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
2758
3333
|
Math.max(1, config.gpuMeshSource.meshes.count) * MESH_RANGE_RECORD_BYTES,
|
|
2759
3334
|
"plasius.wavefront.meshRanges"
|
|
2760
3335
|
);
|
|
3336
|
+
const environmentPortalBuffer = createBuffer(
|
|
3337
|
+
device,
|
|
3338
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
3339
|
+
Math.max(1, config.environmentPortalCapacity) * ENVIRONMENT_PORTAL_RECORD_BYTES,
|
|
3340
|
+
"plasius.wavefront.environmentPortals"
|
|
3341
|
+
);
|
|
2761
3342
|
const bvhLeafRefBuffer = createBuffer(
|
|
2762
3343
|
device,
|
|
2763
3344
|
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2764
3345
|
Math.max(1, config.bvhLeafSortCapacity) * BVH_LEAF_REF_RECORD_BYTES,
|
|
2765
3346
|
"plasius.wavefront.bvhLeafRefs"
|
|
2766
3347
|
);
|
|
2767
|
-
const
|
|
2768
|
-
device,
|
|
2769
|
-
constants.buffer.UNIFORM | constants.buffer.COPY_DST,
|
|
2770
|
-
CONFIG_BUFFER_BYTES,
|
|
2771
|
-
"plasius.wavefront.frameConfig"
|
|
2772
|
-
);
|
|
3348
|
+
const tiles = createTiles(config.width, config.height, config.tileSize);
|
|
2773
3349
|
const uniformOffsetAlignment = Number(device?.limits?.minUniformBufferOffsetAlignment);
|
|
2774
3350
|
const configBufferStride = alignTo(
|
|
2775
3351
|
CONFIG_BUFFER_BYTES,
|
|
@@ -2777,6 +3353,16 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
2777
3353
|
? uniformOffsetAlignment
|
|
2778
3354
|
: CONFIG_BUFFER_BYTES
|
|
2779
3355
|
);
|
|
3356
|
+
const frameConfigSlotCount = Math.max(
|
|
3357
|
+
1,
|
|
3358
|
+
tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
|
|
3359
|
+
);
|
|
3360
|
+
const configBuffer = createBuffer(
|
|
3361
|
+
device,
|
|
3362
|
+
constants.buffer.UNIFORM | constants.buffer.COPY_DST,
|
|
3363
|
+
frameConfigSlotCount * configBufferStride,
|
|
3364
|
+
"plasius.wavefront.frameConfig"
|
|
3365
|
+
);
|
|
2780
3366
|
const bvhBuildConfigSlots =
|
|
2781
3367
|
1 + config.bvhSortStages.length + config.bvhBuildLevels.length;
|
|
2782
3368
|
const bvhBuildConfigBuffer = createBuffer(
|
|
@@ -2812,6 +3398,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
2812
3398
|
device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
|
|
2813
3399
|
device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
|
|
2814
3400
|
device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
|
|
3401
|
+
const packedEnvironmentPortals = packEnvironmentPortals(
|
|
3402
|
+
config.environmentPortals,
|
|
3403
|
+
Math.max(1, config.environmentPortalCapacity)
|
|
3404
|
+
);
|
|
3405
|
+
device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
|
|
2815
3406
|
|
|
2816
3407
|
const radianceTexture = device.createTexture({
|
|
2817
3408
|
label: "plasius.wavefront.radiance",
|
|
@@ -2873,6 +3464,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
2873
3464
|
visibility: constants.shader.COMPUTE,
|
|
2874
3465
|
storageTexture: { access: "write-only", format: "rgba16float" },
|
|
2875
3466
|
},
|
|
3467
|
+
{ binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
2876
3468
|
],
|
|
2877
3469
|
});
|
|
2878
3470
|
const accelerationBindGroupLayout = device.createBindGroupLayout({
|
|
@@ -3048,6 +3640,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3048
3640
|
{ binding: 8, resource: { buffer: triangleBuffer } },
|
|
3049
3641
|
{ binding: 9, resource: { buffer: bvhNodeBuffer } },
|
|
3050
3642
|
{ binding: 16, resource: radianceView },
|
|
3643
|
+
{ binding: 19, resource: { buffer: environmentPortalBuffer } },
|
|
3051
3644
|
],
|
|
3052
3645
|
});
|
|
3053
3646
|
}
|
|
@@ -3139,11 +3732,29 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3139
3732
|
});
|
|
3140
3733
|
|
|
3141
3734
|
let frame = 0;
|
|
3142
|
-
|
|
3735
|
+
let accelerationBuilt = !config.gpuAccelerationBuildRequired;
|
|
3736
|
+
let accelerationBuildCount = 0;
|
|
3737
|
+
|
|
3738
|
+
function createFrameConfigWriter(frameIndex) {
|
|
3739
|
+
let slot = 0;
|
|
3740
|
+
return (tile, buildRange = {}) => {
|
|
3741
|
+
if (slot >= frameConfigSlotCount) {
|
|
3742
|
+
throw new Error("Wavefront frame config slot capacity exceeded.");
|
|
3743
|
+
}
|
|
3744
|
+
const offset = slot * configBufferStride;
|
|
3745
|
+
slot += 1;
|
|
3746
|
+
device.queue.writeBuffer(
|
|
3747
|
+
configBuffer,
|
|
3748
|
+
offset,
|
|
3749
|
+
createConfigPayload(config, tile, frameIndex, buildRange)
|
|
3750
|
+
);
|
|
3751
|
+
return offset;
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3143
3754
|
|
|
3144
3755
|
function dispatchGpuAccelerationBuild(frameIndex) {
|
|
3145
|
-
if (!config.gpuAccelerationBuildRequired) {
|
|
3146
|
-
return;
|
|
3756
|
+
if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
|
|
3757
|
+
return false;
|
|
3147
3758
|
}
|
|
3148
3759
|
const buildTile = tiles[0] ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
3149
3760
|
const encoder = device.createCommandEncoder({
|
|
@@ -3201,30 +3812,24 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3201
3812
|
}
|
|
3202
3813
|
passEncoder.end();
|
|
3203
3814
|
device.queue.submit([encoder.finish()]);
|
|
3815
|
+
accelerationBuilt = true;
|
|
3816
|
+
accelerationBuildCount += 1;
|
|
3817
|
+
return true;
|
|
3204
3818
|
}
|
|
3205
3819
|
|
|
3206
|
-
function
|
|
3207
|
-
const sampleWeight = 1 / config.samplesPerPixel;
|
|
3208
|
-
const configPayload = createConfigPayload(config, tile, frameIndex, {
|
|
3209
|
-
sampleIndex,
|
|
3210
|
-
sampleWeight,
|
|
3211
|
-
});
|
|
3212
|
-
device.queue.writeBuffer(configBuffer, 0, configPayload);
|
|
3213
|
-
const encoder = device.createCommandEncoder({
|
|
3214
|
-
label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.sample.${sampleIndex}`,
|
|
3215
|
-
});
|
|
3820
|
+
function encodeTileSample(encoder, tile, configOffset) {
|
|
3216
3821
|
const passEncoder = encoder.beginComputePass({
|
|
3217
3822
|
label: "plasius.wavefront.computePass",
|
|
3218
3823
|
});
|
|
3219
3824
|
const tileWorkgroups = Math.ceil((tile.width * tile.height) / WORKGROUP_SIZE);
|
|
3220
3825
|
const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
|
|
3221
3826
|
|
|
3222
|
-
passEncoder.setBindGroup(0, bindGroups[0], [
|
|
3827
|
+
passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
|
|
3223
3828
|
passEncoder.setPipeline(pipelines.generatePrimaryRays);
|
|
3224
3829
|
passEncoder.dispatchWorkgroups(tileWorkgroups);
|
|
3225
3830
|
|
|
3226
3831
|
for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
|
|
3227
|
-
passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [
|
|
3832
|
+
passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
|
|
3228
3833
|
passEncoder.setPipeline(pipelines.intersectActiveQueue);
|
|
3229
3834
|
passEncoder.dispatchWorkgroups(capacityWorkgroups);
|
|
3230
3835
|
passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
|
|
@@ -3234,58 +3839,28 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3234
3839
|
}
|
|
3235
3840
|
|
|
3236
3841
|
passEncoder.end();
|
|
3237
|
-
device.queue.submit([encoder.finish()]);
|
|
3238
3842
|
}
|
|
3239
3843
|
|
|
3240
|
-
function
|
|
3241
|
-
const configPayload = createConfigPayload(config, tile, frameIndex, {
|
|
3242
|
-
sampleIndex: 0,
|
|
3243
|
-
sampleWeight: 1 / config.samplesPerPixel,
|
|
3244
|
-
});
|
|
3245
|
-
device.queue.writeBuffer(configBuffer, 0, configPayload);
|
|
3246
|
-
const encoder = device.createCommandEncoder({
|
|
3247
|
-
label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.output`,
|
|
3248
|
-
});
|
|
3844
|
+
function encodeTileOutput(encoder, tile, configOffset) {
|
|
3249
3845
|
const passEncoder = encoder.beginComputePass({
|
|
3250
3846
|
label: "plasius.wavefront.outputPass",
|
|
3251
3847
|
});
|
|
3252
3848
|
const tileWorkgroups = Math.ceil((tile.width * tile.height) / WORKGROUP_SIZE);
|
|
3253
3849
|
|
|
3254
|
-
passEncoder.setBindGroup(0, bindGroups[0], [
|
|
3850
|
+
passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
|
|
3255
3851
|
passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
|
|
3256
3852
|
passEncoder.dispatchWorkgroups(tileWorkgroups);
|
|
3257
3853
|
passEncoder.end();
|
|
3258
|
-
device.queue.submit([encoder.finish()]);
|
|
3259
|
-
}
|
|
3260
|
-
|
|
3261
|
-
function dispatchTile(tile, frameIndex) {
|
|
3262
|
-
for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
|
|
3263
|
-
dispatchTileSample(tile, frameIndex, sampleIndex);
|
|
3264
|
-
}
|
|
3265
|
-
dispatchTileOutput(tile, frameIndex);
|
|
3266
3854
|
}
|
|
3267
3855
|
|
|
3268
|
-
function
|
|
3856
|
+
function encodeDenoise(encoder, configOffset) {
|
|
3269
3857
|
if (!config.denoise) {
|
|
3270
3858
|
return;
|
|
3271
3859
|
}
|
|
3272
|
-
device.queue.writeBuffer(
|
|
3273
|
-
configBuffer,
|
|
3274
|
-
0,
|
|
3275
|
-
createConfigPayload(
|
|
3276
|
-
config,
|
|
3277
|
-
{ x: 0, y: 0, width: config.width, height: config.height },
|
|
3278
|
-
frameIndex,
|
|
3279
|
-
{ sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
|
|
3280
|
-
)
|
|
3281
|
-
);
|
|
3282
|
-
const encoder = device.createCommandEncoder({
|
|
3283
|
-
label: `plasius.wavefront.frame.${frameIndex}.denoise`,
|
|
3284
|
-
});
|
|
3285
3860
|
const radiancePass = encoder.beginComputePass({
|
|
3286
3861
|
label: "plasius.wavefront.denoiseRadiancePass",
|
|
3287
3862
|
});
|
|
3288
|
-
radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [
|
|
3863
|
+
radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
|
|
3289
3864
|
radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
|
|
3290
3865
|
radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
|
|
3291
3866
|
radiancePass.end();
|
|
@@ -3293,18 +3868,14 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3293
3868
|
const resolvePass = encoder.beginComputePass({
|
|
3294
3869
|
label: "plasius.wavefront.denoiseResolvePass",
|
|
3295
3870
|
});
|
|
3296
|
-
resolvePass.setBindGroup(0, denoiseResolveBindGroup, [
|
|
3871
|
+
resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
|
|
3297
3872
|
resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
|
|
3298
3873
|
resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
|
|
3299
3874
|
resolvePass.end();
|
|
3300
|
-
device.queue.submit([encoder.finish()]);
|
|
3301
3875
|
}
|
|
3302
3876
|
|
|
3303
|
-
function
|
|
3877
|
+
function encodePresent(encoder) {
|
|
3304
3878
|
const texture = context.getCurrentTexture();
|
|
3305
|
-
const encoder = device.createCommandEncoder({
|
|
3306
|
-
label: `plasius.wavefront.present.${frame}`,
|
|
3307
|
-
});
|
|
3308
3879
|
const passEncoder = encoder.beginRenderPass({
|
|
3309
3880
|
label: "plasius.wavefront.presentPass",
|
|
3310
3881
|
colorAttachments: [
|
|
@@ -3320,17 +3891,44 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3320
3891
|
passEncoder.setBindGroup(0, presentBindGroup);
|
|
3321
3892
|
passEncoder.draw(3);
|
|
3322
3893
|
passEncoder.end();
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
function dispatchFrame(frameIndex) {
|
|
3897
|
+
const writeFrameConfig = createFrameConfigWriter(frameIndex);
|
|
3898
|
+
const encoder = device.createCommandEncoder({
|
|
3899
|
+
label: `plasius.wavefront.frame.${frameIndex}.batched`,
|
|
3900
|
+
});
|
|
3901
|
+
for (const tile of tiles) {
|
|
3902
|
+
for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
|
|
3903
|
+
const configOffset = writeFrameConfig(tile, {
|
|
3904
|
+
sampleIndex,
|
|
3905
|
+
sampleWeight: 1 / config.samplesPerPixel,
|
|
3906
|
+
});
|
|
3907
|
+
encodeTileSample(encoder, tile, configOffset);
|
|
3908
|
+
}
|
|
3909
|
+
const outputConfigOffset = writeFrameConfig(tile, {
|
|
3910
|
+
sampleIndex: 0,
|
|
3911
|
+
sampleWeight: 1 / config.samplesPerPixel,
|
|
3912
|
+
});
|
|
3913
|
+
encodeTileOutput(encoder, tile, outputConfigOffset);
|
|
3914
|
+
}
|
|
3915
|
+
if (config.denoise) {
|
|
3916
|
+
const denoiseConfigOffset = writeFrameConfig(
|
|
3917
|
+
{ x: 0, y: 0, width: config.width, height: config.height },
|
|
3918
|
+
{ sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
|
|
3919
|
+
);
|
|
3920
|
+
encodeDenoise(encoder, denoiseConfigOffset);
|
|
3921
|
+
}
|
|
3922
|
+
encodePresent(encoder);
|
|
3323
3923
|
device.queue.submit([encoder.finish()]);
|
|
3924
|
+
return 1;
|
|
3324
3925
|
}
|
|
3325
3926
|
|
|
3326
3927
|
function renderOnce() {
|
|
3327
3928
|
frame += 1;
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
}
|
|
3332
|
-
dispatchDenoise(frame + config.frameIndex);
|
|
3333
|
-
present();
|
|
3929
|
+
const frameIndex = frame + config.frameIndex;
|
|
3930
|
+
const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
|
|
3931
|
+
const frameSubmissionCount = dispatchFrame(frameIndex);
|
|
3334
3932
|
return Object.freeze({
|
|
3335
3933
|
frame,
|
|
3336
3934
|
width: config.width,
|
|
@@ -3344,10 +3942,17 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3344
3942
|
sceneObjectCount: config.sceneObjectCount,
|
|
3345
3943
|
triangleCount: config.triangleCount,
|
|
3346
3944
|
emissiveTriangleCount: config.emissiveTriangleCount,
|
|
3945
|
+
environmentPortalCount: config.environmentPortalCount,
|
|
3946
|
+
environmentPortalMode: config.environmentPortalMode,
|
|
3347
3947
|
bvhNodeCount: config.bvhNodeCount,
|
|
3348
3948
|
displayQuality: config.displayQuality,
|
|
3349
3949
|
accelerationBuildMode: config.accelerationBuildMode,
|
|
3350
3950
|
gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
|
|
3951
|
+
accelerationBuildSubmitted,
|
|
3952
|
+
accelerationBuilt,
|
|
3953
|
+
accelerationBuildCount,
|
|
3954
|
+
commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
|
|
3955
|
+
frameConfigSlots: frameConfigSlotCount,
|
|
3351
3956
|
memory: config.memory,
|
|
3352
3957
|
});
|
|
3353
3958
|
}
|
|
@@ -3416,10 +4021,15 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3416
4021
|
sceneObjectCount: config.sceneObjectCount,
|
|
3417
4022
|
triangleCount: config.triangleCount,
|
|
3418
4023
|
emissiveTriangleCount: config.emissiveTriangleCount,
|
|
4024
|
+
environmentPortalCount: config.environmentPortalCount,
|
|
4025
|
+
environmentPortalMode: config.environmentPortalMode,
|
|
3419
4026
|
bvhNodeCount: config.bvhNodeCount,
|
|
3420
4027
|
displayQuality: config.displayQuality,
|
|
3421
4028
|
accelerationBuildMode: config.accelerationBuildMode,
|
|
3422
4029
|
gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
|
|
4030
|
+
accelerationBuilt,
|
|
4031
|
+
accelerationBuildCount,
|
|
4032
|
+
frameConfigSlots: frameConfigSlotCount,
|
|
3423
4033
|
memory: config.memory,
|
|
3424
4034
|
});
|
|
3425
4035
|
}
|
|
@@ -3435,6 +4045,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3435
4045
|
meshVertexBuffer.destroy?.();
|
|
3436
4046
|
meshIndexBuffer.destroy?.();
|
|
3437
4047
|
meshRangeBuffer.destroy?.();
|
|
4048
|
+
environmentPortalBuffer.destroy?.();
|
|
3438
4049
|
bvhLeafRefBuffer.destroy?.();
|
|
3439
4050
|
configBuffer.destroy?.();
|
|
3440
4051
|
bvhBuildConfigBuffer.destroy?.();
|