@plasius/gpu-renderer 0.1.13 → 0.1.15
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 +82 -4
- package/README.md +127 -2
- package/dist/index.cjs +3581 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3560 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/index.d.ts +551 -0
- package/src/index.js +20 -0
- package/src/wavefront-compute.js +3791 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3537 @@
|
|
|
1
|
+
// src/wavefront-compute.js
|
|
2
|
+
var DEFAULT_WIDTH = 1280;
|
|
3
|
+
var DEFAULT_HEIGHT = 720;
|
|
4
|
+
var DEFAULT_MAX_DEPTH = 6;
|
|
5
|
+
var DEFAULT_TILE_SIZE = 128;
|
|
6
|
+
var DEFAULT_SAMPLES_PER_PIXEL = 1;
|
|
7
|
+
var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
|
|
8
|
+
var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
|
|
9
|
+
var WORKGROUP_SIZE = 64;
|
|
10
|
+
var RAY_RECORD_BYTES = 80;
|
|
11
|
+
var HIT_RECORD_BYTES = 208;
|
|
12
|
+
var SCENE_OBJECT_RECORD_BYTES = 96;
|
|
13
|
+
var MESH_VERTEX_RECORD_BYTES = 48;
|
|
14
|
+
var MESH_RANGE_RECORD_BYTES = 96;
|
|
15
|
+
var TRIANGLE_RECORD_BYTES = 208;
|
|
16
|
+
var BVH_NODE_RECORD_BYTES = 48;
|
|
17
|
+
var BVH_LEAF_REF_RECORD_BYTES = 16;
|
|
18
|
+
var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
|
|
19
|
+
var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
|
|
20
|
+
var ACCUMULATION_RECORD_BYTES = 16;
|
|
21
|
+
var CONFIG_BUFFER_BYTES = 272;
|
|
22
|
+
var COUNTER_BUFFER_BYTES = 16;
|
|
23
|
+
var TRACE_STORAGE_BUFFER_BINDINGS = 9;
|
|
24
|
+
var MATERIAL_DIFFUSE = 0;
|
|
25
|
+
var MATERIAL_METAL = 1;
|
|
26
|
+
var MATERIAL_DIELECTRIC = 2;
|
|
27
|
+
var MATERIAL_TRANSPARENT = 3;
|
|
28
|
+
var MATERIAL_EMISSIVE = 4;
|
|
29
|
+
var OBJECT_KIND_SPHERE = 1;
|
|
30
|
+
var OBJECT_KIND_BOX = 2;
|
|
31
|
+
var DEFAULT_CAMERA = Object.freeze({
|
|
32
|
+
position: Object.freeze([0, 1.15, 5.6]),
|
|
33
|
+
target: Object.freeze([0, 0.65, 0]),
|
|
34
|
+
up: Object.freeze([0, 1, 0]),
|
|
35
|
+
fovYDegrees: 46
|
|
36
|
+
});
|
|
37
|
+
var DEFAULT_ENVIRONMENT_COLOR = Object.freeze([0.35, 0.43, 0.49, 1]);
|
|
38
|
+
var DEFAULT_AMBIENT_COLOR = Object.freeze([0.018, 0.022, 0.026, 1]);
|
|
39
|
+
var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
|
|
40
|
+
horizonColor: Object.freeze([0.46, 0.56, 0.68, 1]),
|
|
41
|
+
zenithColor: Object.freeze([0.04, 0.08, 0.16, 1]),
|
|
42
|
+
sunDirection: Object.freeze([0.22, 0.88, 0.42]),
|
|
43
|
+
sunColor: Object.freeze([2.8, 2.65, 2.35, 1]),
|
|
44
|
+
intensity: 1,
|
|
45
|
+
mode: 0,
|
|
46
|
+
exposure: 1
|
|
47
|
+
});
|
|
48
|
+
var wavefrontPathTracingComputeLimits = Object.freeze({
|
|
49
|
+
workgroupSize: WORKGROUP_SIZE,
|
|
50
|
+
traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
|
|
51
|
+
rayRecordBytes: RAY_RECORD_BYTES,
|
|
52
|
+
hitRecordBytes: HIT_RECORD_BYTES,
|
|
53
|
+
sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
|
|
54
|
+
meshVertexRecordBytes: MESH_VERTEX_RECORD_BYTES,
|
|
55
|
+
meshRangeRecordBytes: MESH_RANGE_RECORD_BYTES,
|
|
56
|
+
triangleRecordBytes: TRIANGLE_RECORD_BYTES,
|
|
57
|
+
bvhNodeRecordBytes: BVH_NODE_RECORD_BYTES,
|
|
58
|
+
bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
|
|
59
|
+
emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
|
|
60
|
+
emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
|
|
61
|
+
environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
|
|
62
|
+
accumulationRecordBytes: ACCUMULATION_RECORD_BYTES
|
|
63
|
+
});
|
|
64
|
+
var wavefrontSceneObjectKinds = Object.freeze({
|
|
65
|
+
sphere: OBJECT_KIND_SPHERE,
|
|
66
|
+
box: OBJECT_KIND_BOX
|
|
67
|
+
});
|
|
68
|
+
var wavefrontMaterialKinds = Object.freeze({
|
|
69
|
+
diffuse: MATERIAL_DIFFUSE,
|
|
70
|
+
metal: MATERIAL_METAL,
|
|
71
|
+
dielectric: MATERIAL_DIELECTRIC,
|
|
72
|
+
transparent: MATERIAL_TRANSPARENT,
|
|
73
|
+
emissive: MATERIAL_EMISSIVE
|
|
74
|
+
});
|
|
75
|
+
function readPositiveInteger(name, value, fallback) {
|
|
76
|
+
if (value === void 0 || value === null) {
|
|
77
|
+
return fallback;
|
|
78
|
+
}
|
|
79
|
+
const numeric = Number(value);
|
|
80
|
+
if (!Number.isInteger(numeric) || numeric <= 0) {
|
|
81
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
82
|
+
}
|
|
83
|
+
return numeric;
|
|
84
|
+
}
|
|
85
|
+
function readNonNegativeInteger(name, value, fallback) {
|
|
86
|
+
if (value === void 0 || value === null) {
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
const numeric = Number(value);
|
|
90
|
+
if (!Number.isInteger(numeric) || numeric < 0) {
|
|
91
|
+
throw new Error(`${name} must be a non-negative integer.`);
|
|
92
|
+
}
|
|
93
|
+
return numeric;
|
|
94
|
+
}
|
|
95
|
+
function readFiniteNumber(name, value, fallback) {
|
|
96
|
+
if (value === void 0 || value === null) {
|
|
97
|
+
return fallback;
|
|
98
|
+
}
|
|
99
|
+
const numeric = Number(value);
|
|
100
|
+
if (!Number.isFinite(numeric)) {
|
|
101
|
+
throw new Error(`${name} must be a finite number.`);
|
|
102
|
+
}
|
|
103
|
+
return numeric;
|
|
104
|
+
}
|
|
105
|
+
function assertAnalyticDisplayQualityPolicy(options = {}) {
|
|
106
|
+
const meshes = Array.isArray(options.meshes) ? options.meshes : options.mesh ? [options.mesh] : [];
|
|
107
|
+
if (options.displayQuality === true && meshes.length === 0) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"Display-quality path tracing requires mesh BVH triangle intersections. The analytic sphere/box wavefront renderer is debug-only."
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function clamp(value, min, max) {
|
|
114
|
+
return Math.max(min, Math.min(max, value));
|
|
115
|
+
}
|
|
116
|
+
function asVec3(value, fallback) {
|
|
117
|
+
if (!Array.isArray(value) && !(ArrayBuffer.isView(value) && value.length >= 3)) {
|
|
118
|
+
return [...fallback];
|
|
119
|
+
}
|
|
120
|
+
return [
|
|
121
|
+
readFiniteNumber("vector[0]", value[0], fallback[0]),
|
|
122
|
+
readFiniteNumber("vector[1]", value[1], fallback[1]),
|
|
123
|
+
readFiniteNumber("vector[2]", value[2], fallback[2])
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
function asColor(value, fallback = [1, 1, 1, 1]) {
|
|
127
|
+
if (!Array.isArray(value) && !(ArrayBuffer.isView(value) && value.length >= 3)) {
|
|
128
|
+
return [...fallback];
|
|
129
|
+
}
|
|
130
|
+
return [
|
|
131
|
+
clamp(readFiniteNumber("color[0]", value[0], fallback[0]), 0, 64),
|
|
132
|
+
clamp(readFiniteNumber("color[1]", value[1], fallback[1]), 0, 64),
|
|
133
|
+
clamp(readFiniteNumber("color[2]", value[2], fallback[2]), 0, 64),
|
|
134
|
+
clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
function emissionPower(emission) {
|
|
138
|
+
return Math.max(0, emission?.[0] ?? 0) + Math.max(0, emission?.[1] ?? 0) + Math.max(0, emission?.[2] ?? 0);
|
|
139
|
+
}
|
|
140
|
+
function asUnitVec3(value, fallback) {
|
|
141
|
+
const vector = asVec3(value, fallback);
|
|
142
|
+
return normalize(vector, fallback);
|
|
143
|
+
}
|
|
144
|
+
function add(a, b) {
|
|
145
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
146
|
+
}
|
|
147
|
+
function subtract(a, b) {
|
|
148
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
149
|
+
}
|
|
150
|
+
function scale(a, scalar) {
|
|
151
|
+
return [a[0] * scalar, a[1] * scalar, a[2] * scalar];
|
|
152
|
+
}
|
|
153
|
+
function dot(a, b) {
|
|
154
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
155
|
+
}
|
|
156
|
+
function cross(a, b) {
|
|
157
|
+
return [
|
|
158
|
+
a[1] * b[2] - a[2] * b[1],
|
|
159
|
+
a[2] * b[0] - a[0] * b[2],
|
|
160
|
+
a[0] * b[1] - a[1] * b[0]
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
function normalize(value, fallback = [0, 0, 1]) {
|
|
164
|
+
const length = Math.hypot(value[0], value[1], value[2]);
|
|
165
|
+
if (!Number.isFinite(length) || length <= 1e-6) {
|
|
166
|
+
return [...fallback];
|
|
167
|
+
}
|
|
168
|
+
return [value[0] / length, value[1] / length, value[2] / length];
|
|
169
|
+
}
|
|
170
|
+
function getArrayLikeLength(value) {
|
|
171
|
+
return Array.isArray(value) || ArrayBuffer.isView(value) ? value.length : 0;
|
|
172
|
+
}
|
|
173
|
+
function readVector(values, index, componentCount, fallback) {
|
|
174
|
+
const offset = index * componentCount;
|
|
175
|
+
const output = [];
|
|
176
|
+
for (let component = 0; component < componentCount; component += 1) {
|
|
177
|
+
output.push(readFiniteNumber("mesh attribute", values?.[offset + component], fallback[component] ?? 0));
|
|
178
|
+
}
|
|
179
|
+
return output;
|
|
180
|
+
}
|
|
181
|
+
function readVector2(values, index, fallback = [0, 0]) {
|
|
182
|
+
return readVector(values, index, 2, fallback);
|
|
183
|
+
}
|
|
184
|
+
function triangleBounds(v0, v1, v2) {
|
|
185
|
+
return {
|
|
186
|
+
min: [
|
|
187
|
+
Math.min(v0[0], v1[0], v2[0]),
|
|
188
|
+
Math.min(v0[1], v1[1], v2[1]),
|
|
189
|
+
Math.min(v0[2], v1[2], v2[2])
|
|
190
|
+
],
|
|
191
|
+
max: [
|
|
192
|
+
Math.max(v0[0], v1[0], v2[0]),
|
|
193
|
+
Math.max(v0[1], v1[1], v2[1]),
|
|
194
|
+
Math.max(v0[2], v1[2], v2[2])
|
|
195
|
+
]
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function mergeBounds(left, right) {
|
|
199
|
+
if (!left) {
|
|
200
|
+
return {
|
|
201
|
+
min: [...right.min],
|
|
202
|
+
max: [...right.max]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
min: [
|
|
207
|
+
Math.min(left.min[0], right.min[0]),
|
|
208
|
+
Math.min(left.min[1], right.min[1]),
|
|
209
|
+
Math.min(left.min[2], right.min[2])
|
|
210
|
+
],
|
|
211
|
+
max: [
|
|
212
|
+
Math.max(left.max[0], right.max[0]),
|
|
213
|
+
Math.max(left.max[1], right.max[1]),
|
|
214
|
+
Math.max(left.max[2], right.max[2])
|
|
215
|
+
]
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function boundsCentroid(bounds) {
|
|
219
|
+
return [
|
|
220
|
+
(bounds.min[0] + bounds.max[0]) * 0.5,
|
|
221
|
+
(bounds.min[1] + bounds.max[1]) * 0.5,
|
|
222
|
+
(bounds.min[2] + bounds.max[2]) * 0.5
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
function readMaterialKind(value) {
|
|
226
|
+
if (typeof value === "number") {
|
|
227
|
+
return clamp(Math.trunc(value), MATERIAL_DIFFUSE, MATERIAL_EMISSIVE);
|
|
228
|
+
}
|
|
229
|
+
switch (value) {
|
|
230
|
+
case "metal":
|
|
231
|
+
case "reflective":
|
|
232
|
+
return MATERIAL_METAL;
|
|
233
|
+
case "dielectric":
|
|
234
|
+
case "refractive":
|
|
235
|
+
case "glass":
|
|
236
|
+
return MATERIAL_DIELECTRIC;
|
|
237
|
+
case "transparent":
|
|
238
|
+
case "transmission":
|
|
239
|
+
return MATERIAL_TRANSPARENT;
|
|
240
|
+
case "emissive":
|
|
241
|
+
case "light":
|
|
242
|
+
return MATERIAL_EMISSIVE;
|
|
243
|
+
case "diffuse":
|
|
244
|
+
default:
|
|
245
|
+
return MATERIAL_DIFFUSE;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function readObjectKind(value) {
|
|
249
|
+
if (typeof value === "number") {
|
|
250
|
+
return value === OBJECT_KIND_BOX ? OBJECT_KIND_BOX : OBJECT_KIND_SPHERE;
|
|
251
|
+
}
|
|
252
|
+
switch (value) {
|
|
253
|
+
case "box":
|
|
254
|
+
case "aabb":
|
|
255
|
+
case "bounds":
|
|
256
|
+
return OBJECT_KIND_BOX;
|
|
257
|
+
case "sphere":
|
|
258
|
+
default:
|
|
259
|
+
return OBJECT_KIND_SPHERE;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function deriveBounds(input) {
|
|
263
|
+
if (Array.isArray(input?.min) && Array.isArray(input?.max)) {
|
|
264
|
+
const min = asVec3(input.min, [-0.5, -0.5, -0.5]);
|
|
265
|
+
const max = asVec3(input.max, [0.5, 0.5, 0.5]);
|
|
266
|
+
return {
|
|
267
|
+
center: scale(add(min, max), 0.5),
|
|
268
|
+
halfExtent: scale(subtract(max, min), 0.5).map((value) => Math.max(value, 1e-3))
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (Array.isArray(input?.bounds?.min) && Array.isArray(input?.bounds?.max)) {
|
|
272
|
+
return deriveBounds(input.bounds);
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
function normalizeWavefrontSceneObject(input = {}, index = 0) {
|
|
277
|
+
const bounds = deriveBounds(input);
|
|
278
|
+
const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
|
|
279
|
+
const center = asVec3(input.center ?? input.position ?? bounds?.center, [0, 0, 0]);
|
|
280
|
+
const radius = readFiniteNumber("radius", input.radius, 0.5);
|
|
281
|
+
const halfExtent = kind === OBJECT_KIND_SPHERE ? [Math.max(radius, 1e-3), Math.max(radius, 1e-3), Math.max(radius, 1e-3)] : asVec3(
|
|
282
|
+
input.halfExtent ?? input.halfExtents ?? input.extents ?? bounds?.halfExtent,
|
|
283
|
+
[0.5, 0.5, 0.5]
|
|
284
|
+
).map((value) => Math.max(value, 1e-3));
|
|
285
|
+
const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
|
|
286
|
+
const color = asColor(
|
|
287
|
+
input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
|
|
288
|
+
[0.72, 0.72, 0.68, 1]
|
|
289
|
+
);
|
|
290
|
+
const emission = asColor(
|
|
291
|
+
input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
|
|
292
|
+
[0, 0, 0, 1]
|
|
293
|
+
);
|
|
294
|
+
return Object.freeze({
|
|
295
|
+
id: readNonNegativeInteger("id", input.id, index + 1),
|
|
296
|
+
kind,
|
|
297
|
+
materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
|
|
298
|
+
flags: readNonNegativeInteger("flags", input.flags, 0),
|
|
299
|
+
center: Object.freeze(center),
|
|
300
|
+
halfExtent: Object.freeze(halfExtent),
|
|
301
|
+
color: Object.freeze(color),
|
|
302
|
+
emission: Object.freeze(emission),
|
|
303
|
+
roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
|
|
304
|
+
metallic: clamp(readFiniteNumber("metallic", input.metallic ?? input.material?.metallic, 0), 0, 1),
|
|
305
|
+
opacity: clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1),
|
|
306
|
+
ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3)
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function createDefaultWavefrontSceneObjects() {
|
|
310
|
+
return Object.freeze([
|
|
311
|
+
normalizeWavefrontSceneObject({
|
|
312
|
+
type: "box",
|
|
313
|
+
id: 1,
|
|
314
|
+
center: [0, -0.08, 0],
|
|
315
|
+
halfExtent: [3.25, 0.08, 3.25],
|
|
316
|
+
color: [0.45, 0.53, 0.54, 1],
|
|
317
|
+
roughness: 0.5
|
|
318
|
+
}),
|
|
319
|
+
normalizeWavefrontSceneObject({
|
|
320
|
+
type: "box",
|
|
321
|
+
id: 2,
|
|
322
|
+
center: [0, 1.25, -1.65],
|
|
323
|
+
halfExtent: [2.45, 1.45, 0.08],
|
|
324
|
+
color: [0.42, 0.41, 0.38, 1],
|
|
325
|
+
roughness: 0.85
|
|
326
|
+
}),
|
|
327
|
+
normalizeWavefrontSceneObject({
|
|
328
|
+
type: "sphere",
|
|
329
|
+
id: 3,
|
|
330
|
+
center: [-0.9, 0.72, 0.05],
|
|
331
|
+
radius: 0.72,
|
|
332
|
+
color: [0.76, 0.72, 0.64, 1],
|
|
333
|
+
materialKind: "metal",
|
|
334
|
+
roughness: 0.08,
|
|
335
|
+
metallic: 0.7
|
|
336
|
+
}),
|
|
337
|
+
normalizeWavefrontSceneObject({
|
|
338
|
+
type: "sphere",
|
|
339
|
+
id: 4,
|
|
340
|
+
center: [0.85, 0.65, -0.05],
|
|
341
|
+
radius: 0.58,
|
|
342
|
+
color: [0.68, 0.82, 0.86, 0.72],
|
|
343
|
+
materialKind: "dielectric",
|
|
344
|
+
roughness: 0.02,
|
|
345
|
+
opacity: 0.72,
|
|
346
|
+
ior: 1.35
|
|
347
|
+
}),
|
|
348
|
+
normalizeWavefrontSceneObject({
|
|
349
|
+
type: "sphere",
|
|
350
|
+
id: 5,
|
|
351
|
+
center: [0, 2.55, -0.65],
|
|
352
|
+
radius: 0.34,
|
|
353
|
+
color: [1, 0.94, 0.78, 1],
|
|
354
|
+
emission: [7.2, 6.5, 4.2, 1],
|
|
355
|
+
materialKind: "emissive"
|
|
356
|
+
})
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
|
|
360
|
+
const positions = input.positions;
|
|
361
|
+
const positionLength = getArrayLikeLength(positions);
|
|
362
|
+
if (positionLength < 9 || positionLength % 3 !== 0) {
|
|
363
|
+
throw new Error("Wavefront mesh positions must contain at least three vec3 vertices.");
|
|
364
|
+
}
|
|
365
|
+
const vertexCount = positionLength / 3;
|
|
366
|
+
const indices = getArrayLikeLength(input.indices) > 0 ? Array.from(input.indices, (value) => readNonNegativeInteger("mesh index", value, 0)) : Array.from({ length: vertexCount }, (_, index) => index);
|
|
367
|
+
if (indices.length < 3 || indices.length % 3 !== 0) {
|
|
368
|
+
throw new Error("Wavefront mesh indices must contain complete triangles.");
|
|
369
|
+
}
|
|
370
|
+
if (indices.some((index) => index >= vertexCount)) {
|
|
371
|
+
throw new Error("Wavefront mesh index references a vertex outside the position buffer.");
|
|
372
|
+
}
|
|
373
|
+
const normals = getArrayLikeLength(input.normals) >= positionLength ? Array.from(input.normals, (value) => readFiniteNumber("mesh normal", value, 0)) : null;
|
|
374
|
+
const uvs = getArrayLikeLength(input.uvs ?? input.texcoords ?? input.uv) >= vertexCount * 2 ? Array.from(
|
|
375
|
+
input.uvs ?? input.texcoords ?? input.uv,
|
|
376
|
+
(value) => readFiniteNumber("mesh uv", value, 0)
|
|
377
|
+
) : null;
|
|
378
|
+
const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
|
|
379
|
+
const color = asColor(
|
|
380
|
+
input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
|
|
381
|
+
[0.72, 0.72, 0.68, 1]
|
|
382
|
+
);
|
|
383
|
+
const emission = asColor(
|
|
384
|
+
input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
|
|
385
|
+
[0, 0, 0, 1]
|
|
386
|
+
);
|
|
387
|
+
return Object.freeze({
|
|
388
|
+
id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
|
|
389
|
+
positions: Object.freeze(Array.from(positions, (value) => readFiniteNumber("mesh position", value, 0))),
|
|
390
|
+
indices: Object.freeze(indices),
|
|
391
|
+
normals: normals ? Object.freeze(normals) : null,
|
|
392
|
+
uvs: uvs ? Object.freeze(uvs) : null,
|
|
393
|
+
materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
|
|
394
|
+
flags: readNonNegativeInteger("mesh flags", input.flags, 0),
|
|
395
|
+
materialRefId: readNonNegativeInteger(
|
|
396
|
+
"mesh materialRefId",
|
|
397
|
+
input.materialRefId ?? input.material?.id ?? input.materialId,
|
|
398
|
+
meshIndex
|
|
399
|
+
),
|
|
400
|
+
mediumRefId: readNonNegativeInteger(
|
|
401
|
+
"mesh mediumRefId",
|
|
402
|
+
input.mediumRefId ?? input.medium?.id ?? input.mediumId,
|
|
403
|
+
0
|
|
404
|
+
),
|
|
405
|
+
color: Object.freeze(color),
|
|
406
|
+
emission: Object.freeze(emission),
|
|
407
|
+
roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
|
|
408
|
+
metallic: clamp(readFiniteNumber("metallic", input.metallic ?? input.material?.metallic, 0), 0, 1),
|
|
409
|
+
opacity: clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1),
|
|
410
|
+
ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3)
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function createMeshTriangleRecords(meshes) {
|
|
414
|
+
const source = Array.isArray(meshes) ? meshes : [];
|
|
415
|
+
let nextTriangleId = 0;
|
|
416
|
+
return source.flatMap((meshInput, meshIndex) => {
|
|
417
|
+
const mesh = normalizeWavefrontMesh(meshInput, meshIndex);
|
|
418
|
+
const triangles = [];
|
|
419
|
+
for (let index = 0; index < mesh.indices.length; index += 3) {
|
|
420
|
+
const a = mesh.indices[index];
|
|
421
|
+
const b = mesh.indices[index + 1];
|
|
422
|
+
const c = mesh.indices[index + 2];
|
|
423
|
+
const v0 = readVector(mesh.positions, a, 3, [0, 0, 0]);
|
|
424
|
+
const v1 = readVector(mesh.positions, b, 3, [0, 0, 0]);
|
|
425
|
+
const v2 = readVector(mesh.positions, c, 3, [0, 0, 0]);
|
|
426
|
+
const faceNormal = normalize(cross(subtract(v1, v0), subtract(v2, v0)), [0, 1, 0]);
|
|
427
|
+
const n0 = mesh.normals ? normalize(readVector(mesh.normals, a, 3, faceNormal), faceNormal) : faceNormal;
|
|
428
|
+
const n1 = mesh.normals ? normalize(readVector(mesh.normals, b, 3, faceNormal), faceNormal) : faceNormal;
|
|
429
|
+
const n2 = mesh.normals ? normalize(readVector(mesh.normals, c, 3, faceNormal), faceNormal) : faceNormal;
|
|
430
|
+
const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
|
|
431
|
+
const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
|
|
432
|
+
const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
|
|
433
|
+
const bounds = triangleBounds(v0, v1, v2);
|
|
434
|
+
triangles.push(
|
|
435
|
+
Object.freeze({
|
|
436
|
+
triangleId: nextTriangleId,
|
|
437
|
+
meshId: mesh.id,
|
|
438
|
+
materialKind: mesh.materialKind,
|
|
439
|
+
flags: mesh.flags,
|
|
440
|
+
materialRefId: mesh.materialRefId,
|
|
441
|
+
mediumRefId: mesh.mediumRefId,
|
|
442
|
+
v0: Object.freeze(v0),
|
|
443
|
+
v1: Object.freeze(v1),
|
|
444
|
+
v2: Object.freeze(v2),
|
|
445
|
+
n0: Object.freeze(n0),
|
|
446
|
+
n1: Object.freeze(n1),
|
|
447
|
+
n2: Object.freeze(n2),
|
|
448
|
+
uv0: Object.freeze(uv0),
|
|
449
|
+
uv1: Object.freeze(uv1),
|
|
450
|
+
uv2: Object.freeze(uv2),
|
|
451
|
+
color: mesh.color,
|
|
452
|
+
emission: mesh.emission,
|
|
453
|
+
material: Object.freeze([mesh.roughness, mesh.metallic, mesh.opacity, mesh.ior]),
|
|
454
|
+
bounds: Object.freeze({
|
|
455
|
+
min: Object.freeze(bounds.min),
|
|
456
|
+
max: Object.freeze(bounds.max)
|
|
457
|
+
}),
|
|
458
|
+
centroid: Object.freeze(boundsCentroid(bounds))
|
|
459
|
+
})
|
|
460
|
+
);
|
|
461
|
+
nextTriangleId += 1;
|
|
462
|
+
}
|
|
463
|
+
return triangles;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function chooseSplitAxis(triangles) {
|
|
467
|
+
const centroidBounds = triangles.reduce(
|
|
468
|
+
(bounds, triangle) => {
|
|
469
|
+
const pointBounds = { min: triangle.centroid, max: triangle.centroid };
|
|
470
|
+
return mergeBounds(bounds, pointBounds);
|
|
471
|
+
},
|
|
472
|
+
null
|
|
473
|
+
);
|
|
474
|
+
const extent = subtract(centroidBounds.max, centroidBounds.min);
|
|
475
|
+
if (extent[0] >= extent[1] && extent[0] >= extent[2]) {
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
return extent[1] >= extent[2] ? 1 : 2;
|
|
479
|
+
}
|
|
480
|
+
function buildBvh(triangles, maxLeafTriangles = 4) {
|
|
481
|
+
if (triangles.length === 0) {
|
|
482
|
+
return Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
|
|
483
|
+
}
|
|
484
|
+
const nodes = [];
|
|
485
|
+
const orderedTriangles = [];
|
|
486
|
+
function buildNode(nodeTriangles) {
|
|
487
|
+
const nodeIndex = nodes.length;
|
|
488
|
+
nodes.push(null);
|
|
489
|
+
const bounds = nodeTriangles.reduce((current, triangle) => mergeBounds(current, triangle.bounds), null);
|
|
490
|
+
if (nodeTriangles.length <= maxLeafTriangles) {
|
|
491
|
+
const firstTriangle = orderedTriangles.length;
|
|
492
|
+
orderedTriangles.push(...nodeTriangles);
|
|
493
|
+
nodes[nodeIndex] = Object.freeze({
|
|
494
|
+
bounds: Object.freeze({
|
|
495
|
+
min: Object.freeze(bounds.min),
|
|
496
|
+
max: Object.freeze(bounds.max)
|
|
497
|
+
}),
|
|
498
|
+
firstTriangle,
|
|
499
|
+
triangleCount: nodeTriangles.length,
|
|
500
|
+
leftChild: 0,
|
|
501
|
+
rightChild: 0
|
|
502
|
+
});
|
|
503
|
+
return nodeIndex;
|
|
504
|
+
}
|
|
505
|
+
const axis = chooseSplitAxis(nodeTriangles);
|
|
506
|
+
const sorted = [...nodeTriangles].sort((left, right) => left.centroid[axis] - right.centroid[axis]);
|
|
507
|
+
const midpoint = Math.max(1, Math.floor(sorted.length / 2));
|
|
508
|
+
const leftChild = buildNode(sorted.slice(0, midpoint));
|
|
509
|
+
const rightChild = buildNode(sorted.slice(midpoint));
|
|
510
|
+
nodes[nodeIndex] = Object.freeze({
|
|
511
|
+
bounds: Object.freeze({
|
|
512
|
+
min: Object.freeze(bounds.min),
|
|
513
|
+
max: Object.freeze(bounds.max)
|
|
514
|
+
}),
|
|
515
|
+
firstTriangle: leftChild,
|
|
516
|
+
triangleCount: 0,
|
|
517
|
+
leftChild,
|
|
518
|
+
rightChild
|
|
519
|
+
});
|
|
520
|
+
return nodeIndex;
|
|
521
|
+
}
|
|
522
|
+
buildNode(triangles);
|
|
523
|
+
return Object.freeze({
|
|
524
|
+
nodes: Object.freeze(nodes),
|
|
525
|
+
triangles: Object.freeze(orderedTriangles)
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function createWavefrontMeshAcceleration(meshes = []) {
|
|
529
|
+
const source = Array.isArray(meshes) ? meshes : [meshes];
|
|
530
|
+
const triangles = createMeshTriangleRecords(source);
|
|
531
|
+
return buildBvh(triangles);
|
|
532
|
+
}
|
|
533
|
+
function estimateMeshSourceShape(meshes) {
|
|
534
|
+
const source = Array.isArray(meshes) ? meshes : [];
|
|
535
|
+
return source.reduce(
|
|
536
|
+
(shape, meshInput, meshIndex) => {
|
|
537
|
+
const mesh = normalizeWavefrontMesh(meshInput, meshIndex);
|
|
538
|
+
return {
|
|
539
|
+
vertexCount: shape.vertexCount + mesh.positions.length / 3,
|
|
540
|
+
indexCount: shape.indexCount + mesh.indices.length,
|
|
541
|
+
meshCount: shape.meshCount + 1,
|
|
542
|
+
triangleCount: shape.triangleCount + mesh.indices.length / 3
|
|
543
|
+
};
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
vertexCount: 0,
|
|
547
|
+
indexCount: 0,
|
|
548
|
+
meshCount: 0,
|
|
549
|
+
triangleCount: 0
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
function estimateBinaryBvhNodeCapacity(triangleCount) {
|
|
554
|
+
return triangleCount <= 0 ? 0 : Math.max(1, triangleCount * 2 - 1);
|
|
555
|
+
}
|
|
556
|
+
function nextPowerOfTwo(value) {
|
|
557
|
+
if (value <= 1) {
|
|
558
|
+
return Math.max(0, value);
|
|
559
|
+
}
|
|
560
|
+
return 2 ** Math.ceil(Math.log2(value));
|
|
561
|
+
}
|
|
562
|
+
function estimateBvhLeafSortCapacity(triangleCount) {
|
|
563
|
+
return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
|
|
564
|
+
}
|
|
565
|
+
function createWavefrontBvhSortStages(itemCountInput) {
|
|
566
|
+
const itemCount = readNonNegativeInteger("itemCount", itemCountInput, 0);
|
|
567
|
+
const sortCount = estimateBvhLeafSortCapacity(itemCount);
|
|
568
|
+
if (sortCount <= 1) {
|
|
569
|
+
return Object.freeze([]);
|
|
570
|
+
}
|
|
571
|
+
const stages = [];
|
|
572
|
+
for (let sequenceSize = 2; sequenceSize <= sortCount; sequenceSize *= 2) {
|
|
573
|
+
for (let compareDistance = sequenceSize / 2; compareDistance >= 1; compareDistance /= 2) {
|
|
574
|
+
stages.push(
|
|
575
|
+
Object.freeze({
|
|
576
|
+
compareDistance,
|
|
577
|
+
sequenceSize
|
|
578
|
+
})
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return Object.freeze(stages);
|
|
583
|
+
}
|
|
584
|
+
function createWavefrontBvhBuildLevels(triangleCountInput) {
|
|
585
|
+
const triangleCount = readNonNegativeInteger("triangleCount", triangleCountInput, 0);
|
|
586
|
+
const internalCount = Math.max(0, triangleCount - 1);
|
|
587
|
+
if (internalCount === 0) {
|
|
588
|
+
return Object.freeze([]);
|
|
589
|
+
}
|
|
590
|
+
const levels = [];
|
|
591
|
+
let depth = 0;
|
|
592
|
+
while (Math.pow(2, depth) - 1 < internalCount) {
|
|
593
|
+
depth += 1;
|
|
594
|
+
}
|
|
595
|
+
for (let level = depth - 1; level >= 0; level -= 1) {
|
|
596
|
+
const start = Math.pow(2, level) - 1;
|
|
597
|
+
const end = Math.min(Math.pow(2, level + 1) - 2, internalCount - 1);
|
|
598
|
+
if (end >= start) {
|
|
599
|
+
levels.push(
|
|
600
|
+
Object.freeze({
|
|
601
|
+
start,
|
|
602
|
+
count: end - start + 1
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return Object.freeze(levels);
|
|
608
|
+
}
|
|
609
|
+
function resolveAccelerationBuildMode(options = {}) {
|
|
610
|
+
const mode = options.accelerationBuildMode ?? (options.displayQuality === true ? "gpu" : "cpu-debug");
|
|
611
|
+
if (mode !== "gpu" && mode !== "cpu-debug") {
|
|
612
|
+
throw new Error('accelerationBuildMode must be either "gpu" or "cpu-debug".');
|
|
613
|
+
}
|
|
614
|
+
if (options.displayQuality === true && mode !== "gpu") {
|
|
615
|
+
throw new Error("Display-quality path tracing requires GPU-built mesh acceleration.");
|
|
616
|
+
}
|
|
617
|
+
return mode;
|
|
618
|
+
}
|
|
619
|
+
function createWavefrontGpuMeshSource(meshes = []) {
|
|
620
|
+
const source = Array.isArray(meshes) ? meshes : [meshes];
|
|
621
|
+
const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
|
|
622
|
+
const vertexCount = normalized.reduce((count, mesh) => count + mesh.positions.length / 3, 0);
|
|
623
|
+
const indexCount = normalized.reduce((count, mesh) => count + mesh.indices.length, 0);
|
|
624
|
+
const triangleCount = Math.floor(indexCount / 3);
|
|
625
|
+
const vertexBytes = new ArrayBuffer(Math.max(1, vertexCount) * MESH_VERTEX_RECORD_BYTES);
|
|
626
|
+
const indexBytes = new ArrayBuffer(Math.max(1, indexCount) * 4);
|
|
627
|
+
const meshBytes = new ArrayBuffer(Math.max(1, normalized.length) * MESH_RANGE_RECORD_BYTES);
|
|
628
|
+
const vertexFloats = new Float32Array(vertexBytes);
|
|
629
|
+
const indexUints = new Uint32Array(indexBytes);
|
|
630
|
+
const meshUints = new Uint32Array(meshBytes);
|
|
631
|
+
const meshFloats = new Float32Array(meshBytes);
|
|
632
|
+
let vertexCursor = 0;
|
|
633
|
+
let indexCursor = 0;
|
|
634
|
+
let triangleCursor = 0;
|
|
635
|
+
normalized.forEach((mesh, meshIndex) => {
|
|
636
|
+
const meshVertexBase = vertexCursor;
|
|
637
|
+
const meshIndexBase = indexCursor;
|
|
638
|
+
const meshTriangleBase = triangleCursor;
|
|
639
|
+
const meshVertexCount = mesh.positions.length / 3;
|
|
640
|
+
for (let vertexIndex = 0; vertexIndex < meshVertexCount; vertexIndex += 1) {
|
|
641
|
+
const recordOffset = (vertexCursor + vertexIndex) * (MESH_VERTEX_RECORD_BYTES / 4);
|
|
642
|
+
const position = readVector(mesh.positions, vertexIndex, 3, [0, 0, 0]);
|
|
643
|
+
const normal = mesh.normals ? readVector(mesh.normals, vertexIndex, 3, [0, 0, 0]) : [0, 0, 0];
|
|
644
|
+
const uv = mesh.uvs ? readVector2(mesh.uvs, vertexIndex) : [0, 0];
|
|
645
|
+
vertexFloats[recordOffset] = position[0];
|
|
646
|
+
vertexFloats[recordOffset + 1] = position[1];
|
|
647
|
+
vertexFloats[recordOffset + 2] = position[2];
|
|
648
|
+
vertexFloats[recordOffset + 3] = 1;
|
|
649
|
+
vertexFloats[recordOffset + 4] = normal[0];
|
|
650
|
+
vertexFloats[recordOffset + 5] = normal[1];
|
|
651
|
+
vertexFloats[recordOffset + 6] = normal[2];
|
|
652
|
+
vertexFloats[recordOffset + 7] = mesh.normals ? 1 : 0;
|
|
653
|
+
vertexFloats[recordOffset + 8] = uv[0];
|
|
654
|
+
vertexFloats[recordOffset + 9] = uv[1];
|
|
655
|
+
vertexFloats[recordOffset + 10] = mesh.uvs ? 1 : 0;
|
|
656
|
+
vertexFloats[recordOffset + 11] = 0;
|
|
657
|
+
}
|
|
658
|
+
mesh.indices.forEach((indexValue, localIndex) => {
|
|
659
|
+
indexUints[indexCursor + localIndex] = meshVertexBase + indexValue;
|
|
660
|
+
});
|
|
661
|
+
const meshOffset = meshIndex * (MESH_RANGE_RECORD_BYTES / 4);
|
|
662
|
+
meshUints[meshOffset] = mesh.id;
|
|
663
|
+
meshUints[meshOffset + 1] = mesh.materialKind;
|
|
664
|
+
meshUints[meshOffset + 2] = mesh.flags;
|
|
665
|
+
meshUints[meshOffset + 3] = mesh.materialRefId;
|
|
666
|
+
meshUints[meshOffset + 4] = mesh.mediumRefId;
|
|
667
|
+
meshUints[meshOffset + 5] = meshIndexBase;
|
|
668
|
+
meshUints[meshOffset + 6] = mesh.indices.length;
|
|
669
|
+
meshUints[meshOffset + 7] = meshTriangleBase;
|
|
670
|
+
meshUints[meshOffset + 8] = mesh.indices.length / 3;
|
|
671
|
+
meshUints[meshOffset + 9] = meshVertexBase;
|
|
672
|
+
meshUints[meshOffset + 10] = meshVertexCount;
|
|
673
|
+
meshUints[meshOffset + 11] = 0;
|
|
674
|
+
const floatOffset = meshOffset;
|
|
675
|
+
writeVec4(meshFloats, floatOffset * 4 + 48, mesh.color);
|
|
676
|
+
writeVec4(meshFloats, floatOffset * 4 + 64, mesh.emission);
|
|
677
|
+
writeVec4(meshFloats, floatOffset * 4 + 80, [
|
|
678
|
+
mesh.roughness,
|
|
679
|
+
mesh.metallic,
|
|
680
|
+
mesh.opacity,
|
|
681
|
+
mesh.ior
|
|
682
|
+
]);
|
|
683
|
+
vertexCursor += meshVertexCount;
|
|
684
|
+
indexCursor += mesh.indices.length;
|
|
685
|
+
triangleCursor += mesh.indices.length / 3;
|
|
686
|
+
});
|
|
687
|
+
return Object.freeze({
|
|
688
|
+
vertices: Object.freeze({
|
|
689
|
+
buffer: vertexBytes,
|
|
690
|
+
count: vertexCount,
|
|
691
|
+
recordBytes: MESH_VERTEX_RECORD_BYTES
|
|
692
|
+
}),
|
|
693
|
+
indices: Object.freeze({
|
|
694
|
+
buffer: indexBytes,
|
|
695
|
+
count: indexCount,
|
|
696
|
+
recordBytes: 4
|
|
697
|
+
}),
|
|
698
|
+
meshes: Object.freeze({
|
|
699
|
+
buffer: meshBytes,
|
|
700
|
+
records: Object.freeze(normalized),
|
|
701
|
+
count: normalized.length,
|
|
702
|
+
recordBytes: MESH_RANGE_RECORD_BYTES
|
|
703
|
+
}),
|
|
704
|
+
triangleCount,
|
|
705
|
+
bvhNodeCapacity: estimateBinaryBvhNodeCapacity(triangleCount)
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function createWavefrontEmissiveTriangleIndexSource(meshes = [], capacityInput) {
|
|
709
|
+
const source = Array.isArray(meshes) ? meshes : [meshes];
|
|
710
|
+
const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
|
|
711
|
+
const indices = [];
|
|
712
|
+
let triangleCursor = 0;
|
|
713
|
+
normalized.forEach((mesh) => {
|
|
714
|
+
const triangleCount = Math.floor(mesh.indices.length / 3);
|
|
715
|
+
const isEmissive = mesh.materialKind === MATERIAL_EMISSIVE || emissionPower(mesh.emission) > 1e-4;
|
|
716
|
+
if (isEmissive) {
|
|
717
|
+
for (let triangleOffset = 0; triangleOffset < triangleCount; triangleOffset += 1) {
|
|
718
|
+
indices.push(triangleCursor + triangleOffset);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
triangleCursor += triangleCount;
|
|
722
|
+
});
|
|
723
|
+
const capacity = Math.max(
|
|
724
|
+
indices.length,
|
|
725
|
+
readNonNegativeInteger("emissiveTriangleCapacity", capacityInput, indices.length)
|
|
726
|
+
);
|
|
727
|
+
const bytes = new ArrayBuffer(capacity * EMISSIVE_TRIANGLE_INDEX_BYTES);
|
|
728
|
+
const uints = new Uint32Array(bytes);
|
|
729
|
+
uints.fill(4294967295);
|
|
730
|
+
indices.forEach((triangleIndex, index) => {
|
|
731
|
+
uints[index] = triangleIndex;
|
|
732
|
+
});
|
|
733
|
+
return Object.freeze({
|
|
734
|
+
buffer: bytes,
|
|
735
|
+
indices: Object.freeze(indices),
|
|
736
|
+
count: indices.length,
|
|
737
|
+
capacity,
|
|
738
|
+
recordBytes: EMISSIVE_TRIANGLE_INDEX_BYTES
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
|
|
742
|
+
const source = Array.isArray(sceneObjects) && sceneObjects.length > 0 ? sceneObjects : useDefaultScene ? createDefaultWavefrontSceneObjects() : [];
|
|
743
|
+
return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
|
|
744
|
+
}
|
|
745
|
+
function normalizeMeshes(options = {}) {
|
|
746
|
+
if (Array.isArray(options.meshes)) {
|
|
747
|
+
return options.meshes;
|
|
748
|
+
}
|
|
749
|
+
if (options.mesh) {
|
|
750
|
+
return [options.mesh];
|
|
751
|
+
}
|
|
752
|
+
return [];
|
|
753
|
+
}
|
|
754
|
+
function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
|
|
755
|
+
const source = input ?? {};
|
|
756
|
+
return Object.freeze({
|
|
757
|
+
environmentColor: Object.freeze(asColor(source.environmentColor, environmentColor)),
|
|
758
|
+
ambientColor: Object.freeze(asColor(source.ambientColor, ambientColor)),
|
|
759
|
+
horizonColor: Object.freeze(asColor(source.horizonColor, environmentColor)),
|
|
760
|
+
zenithColor: Object.freeze(asColor(source.zenithColor, DEFAULT_ENVIRONMENT_LIGHTING.zenithColor)),
|
|
761
|
+
sunDirection: Object.freeze(asUnitVec3(source.sunDirection, DEFAULT_ENVIRONMENT_LIGHTING.sunDirection)),
|
|
762
|
+
sunColor: Object.freeze(asColor(source.sunColor, DEFAULT_ENVIRONMENT_LIGHTING.sunColor)),
|
|
763
|
+
intensity: Math.max(1e-4, readFiniteNumber("environmentLighting.intensity", source.intensity, DEFAULT_ENVIRONMENT_LIGHTING.intensity)),
|
|
764
|
+
mode: readNonNegativeInteger("environmentLighting.mode", source.mode, DEFAULT_ENVIRONMENT_LIGHTING.mode),
|
|
765
|
+
exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function resolveEnvironmentPortalMode(value, hasPortals) {
|
|
769
|
+
if (value === void 0 || value === null) {
|
|
770
|
+
return hasPortals ? 2 : 0;
|
|
771
|
+
}
|
|
772
|
+
if (Number.isInteger(value) && value >= 0 && value <= 2) {
|
|
773
|
+
return value;
|
|
774
|
+
}
|
|
775
|
+
if (value === "disabled") {
|
|
776
|
+
return 0;
|
|
777
|
+
}
|
|
778
|
+
if (value === "guide") {
|
|
779
|
+
return 1;
|
|
780
|
+
}
|
|
781
|
+
if (value === "guide-and-gate" || value === "gate") {
|
|
782
|
+
return 2;
|
|
783
|
+
}
|
|
784
|
+
throw new Error(
|
|
785
|
+
"environmentPortalMode must be disabled, guide, guide-and-gate, or an integer between 0 and 2."
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
function orthogonalPortalTangent(normal) {
|
|
789
|
+
if (Math.abs(normal[1]) < 0.92) {
|
|
790
|
+
return normalize(cross([0, 1, 0], normal), [1, 0, 0]);
|
|
791
|
+
}
|
|
792
|
+
return normalize(cross([1, 0, 0], normal), [0, 0, 1]);
|
|
793
|
+
}
|
|
794
|
+
function resolvePortalTangent(value, normal) {
|
|
795
|
+
const fallback = orthogonalPortalTangent(normal);
|
|
796
|
+
const tangent = asUnitVec3(value, fallback);
|
|
797
|
+
const projected = subtract(tangent, scale(normal, dot(tangent, normal)));
|
|
798
|
+
return normalize(projected, fallback);
|
|
799
|
+
}
|
|
800
|
+
function readPositiveFiniteNumber(name, value, fallback) {
|
|
801
|
+
const numeric = readFiniteNumber(name, value, fallback);
|
|
802
|
+
if (numeric <= 0) {
|
|
803
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
804
|
+
}
|
|
805
|
+
return numeric;
|
|
806
|
+
}
|
|
807
|
+
function readPortalExtent(name, value, halfName, halfValue) {
|
|
808
|
+
if (value !== void 0 && value !== null) {
|
|
809
|
+
return readPositiveFiniteNumber(name, value, 1);
|
|
810
|
+
}
|
|
811
|
+
return readPositiveFiniteNumber(halfName, halfValue, 0.5) * 2;
|
|
812
|
+
}
|
|
813
|
+
function normalizeEnvironmentPortal(portal, index) {
|
|
814
|
+
if (!portal || typeof portal !== "object") {
|
|
815
|
+
throw new Error(`environmentPortals[${index}] must be an object.`);
|
|
816
|
+
}
|
|
817
|
+
const shape = portal.shape ?? portal.kind ?? "rectangle";
|
|
818
|
+
if (shape !== "rectangle") {
|
|
819
|
+
throw new Error(`environmentPortals[${index}].shape must be "rectangle".`);
|
|
820
|
+
}
|
|
821
|
+
const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
|
|
822
|
+
const normal = asUnitVec3(portal.normal, [0, 0, 1]);
|
|
823
|
+
const tangent = resolvePortalTangent(portal.tangent, normal);
|
|
824
|
+
const bitangent = normalize(cross(normal, tangent), [0, 1, 0]);
|
|
825
|
+
const width = readPortalExtent(
|
|
826
|
+
`environmentPortals[${index}].width`,
|
|
827
|
+
portal.width,
|
|
828
|
+
`environmentPortals[${index}].halfWidth`,
|
|
829
|
+
portal.halfWidth
|
|
830
|
+
);
|
|
831
|
+
const height = readPortalExtent(
|
|
832
|
+
`environmentPortals[${index}].height`,
|
|
833
|
+
portal.height,
|
|
834
|
+
`environmentPortals[${index}].halfHeight`,
|
|
835
|
+
portal.halfHeight
|
|
836
|
+
);
|
|
837
|
+
const radianceScale = Math.max(
|
|
838
|
+
0,
|
|
839
|
+
readFiniteNumber(
|
|
840
|
+
`environmentPortals[${index}].radianceScale`,
|
|
841
|
+
portal.radianceScale ?? portal.intensity,
|
|
842
|
+
1
|
|
843
|
+
)
|
|
844
|
+
);
|
|
845
|
+
return Object.freeze({
|
|
846
|
+
kind: 1,
|
|
847
|
+
flags: portal.twoSided === false ? 0 : 1,
|
|
848
|
+
position: Object.freeze([position[0], position[1], position[2], width * height]),
|
|
849
|
+
normal: Object.freeze([normal[0], normal[1], normal[2], radianceScale]),
|
|
850
|
+
tangent: Object.freeze([tangent[0], tangent[1], tangent[2], width * 0.5]),
|
|
851
|
+
bitangent: Object.freeze([bitangent[0], bitangent[1], bitangent[2], height * 0.5]),
|
|
852
|
+
color: Object.freeze(asColor(portal.color, [1, 1, 1, 1]))
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
function normalizeEnvironmentPortals(value) {
|
|
856
|
+
if (value === void 0 || value === null) {
|
|
857
|
+
return Object.freeze([]);
|
|
858
|
+
}
|
|
859
|
+
if (!Array.isArray(value)) {
|
|
860
|
+
throw new Error("environmentPortals must be an array when provided.");
|
|
861
|
+
}
|
|
862
|
+
return Object.freeze(value.map(normalizeEnvironmentPortal));
|
|
863
|
+
}
|
|
864
|
+
function packEnvironmentPortals(portals, capacity) {
|
|
865
|
+
const bytes = new ArrayBuffer(capacity * ENVIRONMENT_PORTAL_RECORD_BYTES);
|
|
866
|
+
const data = new DataView(bytes);
|
|
867
|
+
const floatView = new Float32Array(bytes);
|
|
868
|
+
portals.forEach((portal, index) => {
|
|
869
|
+
const byteOffset = index * ENVIRONMENT_PORTAL_RECORD_BYTES;
|
|
870
|
+
const floatOffset = byteOffset / Float32Array.BYTES_PER_ELEMENT;
|
|
871
|
+
data.setUint32(byteOffset, portal.kind, true);
|
|
872
|
+
data.setUint32(byteOffset + 4, portal.flags, true);
|
|
873
|
+
data.setUint32(byteOffset + 8, 0, true);
|
|
874
|
+
data.setUint32(byteOffset + 12, 0, true);
|
|
875
|
+
writeVec4(floatView, floatOffset + 4, portal.position);
|
|
876
|
+
writeVec4(floatView, floatOffset + 8, portal.normal);
|
|
877
|
+
writeVec4(floatView, floatOffset + 12, portal.tangent);
|
|
878
|
+
writeVec4(floatView, floatOffset + 16, portal.bitangent);
|
|
879
|
+
writeVec4(floatView, floatOffset + 20, portal.color);
|
|
880
|
+
});
|
|
881
|
+
return Object.freeze({
|
|
882
|
+
buffer: bytes,
|
|
883
|
+
portals,
|
|
884
|
+
count: portals.length,
|
|
885
|
+
capacity,
|
|
886
|
+
recordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
function getCanvasDimension(canvas, key, fallback) {
|
|
890
|
+
const value = Number(canvas?.[key]);
|
|
891
|
+
if (Number.isFinite(value) && value > 0) {
|
|
892
|
+
return Math.trunc(value);
|
|
893
|
+
}
|
|
894
|
+
return fallback;
|
|
895
|
+
}
|
|
896
|
+
function resolveCamera(input, width, height) {
|
|
897
|
+
const camera = input ?? DEFAULT_CAMERA;
|
|
898
|
+
const position = asVec3(camera.position, DEFAULT_CAMERA.position);
|
|
899
|
+
const target = asVec3(camera.target, DEFAULT_CAMERA.target);
|
|
900
|
+
const upInput = normalize(asVec3(camera.up, DEFAULT_CAMERA.up), DEFAULT_CAMERA.up);
|
|
901
|
+
const forward = normalize(subtract(target, position), [0, 0, -1]);
|
|
902
|
+
const right = normalize(cross(forward, upInput), [1, 0, 0]);
|
|
903
|
+
const up = normalize(cross(right, forward), [0, 1, 0]);
|
|
904
|
+
const fovYDegrees = clamp(
|
|
905
|
+
readFiniteNumber("camera.fovYDegrees", camera.fovYDegrees ?? camera.fov, DEFAULT_CAMERA.fovYDegrees),
|
|
906
|
+
10,
|
|
907
|
+
120
|
|
908
|
+
);
|
|
909
|
+
const aspect = width / Math.max(1, height);
|
|
910
|
+
const tanHalfFovY = Math.tan(fovYDegrees * Math.PI / 360);
|
|
911
|
+
return Object.freeze({
|
|
912
|
+
position: Object.freeze(position),
|
|
913
|
+
forward: Object.freeze(forward),
|
|
914
|
+
right: Object.freeze(right),
|
|
915
|
+
up: Object.freeze(up),
|
|
916
|
+
fovYDegrees,
|
|
917
|
+
aspect,
|
|
918
|
+
tanHalfFovY
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
function estimateWavefrontPathTracingMemory(options = {}) {
|
|
922
|
+
const tilePixelCapacity = readPositiveInteger(
|
|
923
|
+
"tilePixelCapacity",
|
|
924
|
+
options.tilePixelCapacity,
|
|
925
|
+
DEFAULT_TILE_SIZE * DEFAULT_TILE_SIZE
|
|
926
|
+
);
|
|
927
|
+
const sceneObjectCapacity = readPositiveInteger(
|
|
928
|
+
"sceneObjectCapacity",
|
|
929
|
+
options.sceneObjectCapacity,
|
|
930
|
+
DEFAULT_SCENE_OBJECT_CAPACITY
|
|
931
|
+
);
|
|
932
|
+
const triangleCapacity = readNonNegativeInteger("triangleCapacity", options.triangleCapacity, 0);
|
|
933
|
+
const bvhNodeCapacity = readNonNegativeInteger("bvhNodeCapacity", options.bvhNodeCapacity, 0);
|
|
934
|
+
const bvhLeafSortCapacity = readNonNegativeInteger(
|
|
935
|
+
"bvhLeafSortCapacity",
|
|
936
|
+
options.bvhLeafSortCapacity,
|
|
937
|
+
0
|
|
938
|
+
);
|
|
939
|
+
const emissiveTriangleCapacity = readNonNegativeInteger(
|
|
940
|
+
"emissiveTriangleCapacity",
|
|
941
|
+
options.emissiveTriangleCapacity,
|
|
942
|
+
0
|
|
943
|
+
);
|
|
944
|
+
const environmentPortalCapacity = readNonNegativeInteger(
|
|
945
|
+
"environmentPortalCapacity",
|
|
946
|
+
options.environmentPortalCapacity,
|
|
947
|
+
0
|
|
948
|
+
);
|
|
949
|
+
const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
|
|
950
|
+
const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
|
|
951
|
+
const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
|
|
952
|
+
const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
|
|
953
|
+
const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
|
|
954
|
+
const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
|
|
955
|
+
const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
|
|
956
|
+
const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
|
|
957
|
+
const environmentPortalBytes = environmentPortalCapacity * ENVIRONMENT_PORTAL_RECORD_BYTES;
|
|
958
|
+
return Object.freeze({
|
|
959
|
+
queueBytes,
|
|
960
|
+
queuePairBytes: queueBytes * 2,
|
|
961
|
+
hitBytes,
|
|
962
|
+
accumulationBytes,
|
|
963
|
+
sceneObjectBytes,
|
|
964
|
+
triangleBytes,
|
|
965
|
+
bvhNodeBytes,
|
|
966
|
+
bvhLeafReferenceBytes,
|
|
967
|
+
emissiveTriangleMetadataBytes,
|
|
968
|
+
environmentPortalBytes,
|
|
969
|
+
configBytes: CONFIG_BUFFER_BYTES,
|
|
970
|
+
counterBytes: COUNTER_BUFFER_BYTES,
|
|
971
|
+
totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
975
|
+
assertAnalyticDisplayQualityPolicy(options);
|
|
976
|
+
const accelerationBuildMode = resolveAccelerationBuildMode(options);
|
|
977
|
+
const canvas = options.canvas;
|
|
978
|
+
const width = readPositiveInteger("width", options.width, getCanvasDimension(canvas, "width", DEFAULT_WIDTH));
|
|
979
|
+
const height = readPositiveInteger("height", options.height, getCanvasDimension(canvas, "height", DEFAULT_HEIGHT));
|
|
980
|
+
const maxDepth = clamp(readPositiveInteger("maxDepth", options.maxDepth, DEFAULT_MAX_DEPTH), 1, 16);
|
|
981
|
+
const tileSize = clamp(readPositiveInteger("tileSize", options.tileSize, DEFAULT_TILE_SIZE), 16, 512);
|
|
982
|
+
const samplesPerPixel = clamp(
|
|
983
|
+
readPositiveInteger("samplesPerPixel", options.samplesPerPixel, DEFAULT_SAMPLES_PER_PIXEL),
|
|
984
|
+
1,
|
|
985
|
+
64
|
|
986
|
+
);
|
|
987
|
+
const tilePixelCapacity = readPositiveInteger(
|
|
988
|
+
"tilePixelCapacity",
|
|
989
|
+
options.tilePixelCapacity,
|
|
990
|
+
tileSize * tileSize
|
|
991
|
+
);
|
|
992
|
+
const meshes = normalizeMeshes(options);
|
|
993
|
+
const meshSourceShape = estimateMeshSourceShape(meshes);
|
|
994
|
+
const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes) : createWavefrontGpuMeshSource([]);
|
|
995
|
+
const meshAcceleration = accelerationBuildMode === "cpu-debug" ? createWavefrontMeshAcceleration(meshes) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
|
|
996
|
+
const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
|
|
997
|
+
meshes,
|
|
998
|
+
options.emissiveTriangleCapacity
|
|
999
|
+
);
|
|
1000
|
+
const triangleCount = accelerationBuildMode === "gpu" ? meshSourceShape.triangleCount : meshAcceleration.triangles.length;
|
|
1001
|
+
const bvhNodeCount = accelerationBuildMode === "gpu" ? estimateBinaryBvhNodeCapacity(triangleCount) : meshAcceleration.nodes.length;
|
|
1002
|
+
const sceneObjects = Object.freeze(
|
|
1003
|
+
normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
|
|
1004
|
+
);
|
|
1005
|
+
const sceneObjectCapacity = Math.max(
|
|
1006
|
+
sceneObjects.length,
|
|
1007
|
+
readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
|
|
1008
|
+
);
|
|
1009
|
+
const triangleCapacity = Math.max(
|
|
1010
|
+
triangleCount,
|
|
1011
|
+
readNonNegativeInteger("triangleCapacity", options.triangleCapacity, triangleCount)
|
|
1012
|
+
);
|
|
1013
|
+
const bvhNodeCapacity = Math.max(
|
|
1014
|
+
accelerationBuildMode === "gpu" ? estimateBinaryBvhNodeCapacity(triangleCount) : bvhNodeCount,
|
|
1015
|
+
readNonNegativeInteger(
|
|
1016
|
+
"bvhNodeCapacity",
|
|
1017
|
+
options.bvhNodeCapacity,
|
|
1018
|
+
accelerationBuildMode === "gpu" ? estimateBinaryBvhNodeCapacity(triangleCount) : bvhNodeCount
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
const bvhLeafSortCapacity = accelerationBuildMode === "gpu" ? estimateBvhLeafSortCapacity(triangleCount) : 0;
|
|
1022
|
+
const bvhSortStages = accelerationBuildMode === "gpu" ? createWavefrontBvhSortStages(triangleCount) : Object.freeze([]);
|
|
1023
|
+
const bvhBuildLevels = accelerationBuildMode === "gpu" ? createWavefrontBvhBuildLevels(triangleCount) : Object.freeze([]);
|
|
1024
|
+
const camera = resolveCamera(options.camera, width, height);
|
|
1025
|
+
const environmentColor = Object.freeze(asColor(options.environmentColor, DEFAULT_ENVIRONMENT_COLOR));
|
|
1026
|
+
const ambientColor = Object.freeze(asColor(options.ambientColor, DEFAULT_AMBIENT_COLOR));
|
|
1027
|
+
const environmentLighting = resolveEnvironmentLighting(
|
|
1028
|
+
options.environmentLighting,
|
|
1029
|
+
environmentColor,
|
|
1030
|
+
ambientColor
|
|
1031
|
+
);
|
|
1032
|
+
const environmentPortals = normalizeEnvironmentPortals(
|
|
1033
|
+
options.environmentPortals ?? options.environmentLightPortals ?? options.environmentLighting?.environmentPortals
|
|
1034
|
+
);
|
|
1035
|
+
const environmentPortalCapacity = Math.max(
|
|
1036
|
+
environmentPortals.length,
|
|
1037
|
+
readNonNegativeInteger(
|
|
1038
|
+
"environmentPortalCapacity",
|
|
1039
|
+
options.environmentPortalCapacity,
|
|
1040
|
+
DEFAULT_ENVIRONMENT_PORTAL_CAPACITY
|
|
1041
|
+
)
|
|
1042
|
+
);
|
|
1043
|
+
const environmentPortalMode = resolveEnvironmentPortalMode(
|
|
1044
|
+
options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
|
|
1045
|
+
environmentPortals.length > 0
|
|
1046
|
+
);
|
|
1047
|
+
return Object.freeze({
|
|
1048
|
+
width,
|
|
1049
|
+
height,
|
|
1050
|
+
maxDepth,
|
|
1051
|
+
tileSize,
|
|
1052
|
+
samplesPerPixel,
|
|
1053
|
+
tilePixelCapacity,
|
|
1054
|
+
sceneObjects,
|
|
1055
|
+
sceneObjectCount: sceneObjects.length,
|
|
1056
|
+
sceneObjectCapacity,
|
|
1057
|
+
accelerationBuildMode,
|
|
1058
|
+
gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
|
|
1059
|
+
gpuMeshSource,
|
|
1060
|
+
meshAcceleration,
|
|
1061
|
+
emissiveTriangleIndices,
|
|
1062
|
+
emissiveTriangleCount: emissiveTriangleIndices.count,
|
|
1063
|
+
emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
|
|
1064
|
+
triangleCount,
|
|
1065
|
+
triangleCapacity,
|
|
1066
|
+
bvhNodeCount,
|
|
1067
|
+
bvhNodeCapacity,
|
|
1068
|
+
bvhLeafSortCapacity,
|
|
1069
|
+
bvhSortStages,
|
|
1070
|
+
bvhBuildLevels,
|
|
1071
|
+
camera,
|
|
1072
|
+
environmentColor: environmentLighting.environmentColor,
|
|
1073
|
+
ambientColor: environmentLighting.ambientColor,
|
|
1074
|
+
environmentLighting,
|
|
1075
|
+
environmentPortals,
|
|
1076
|
+
environmentPortalCount: environmentPortals.length,
|
|
1077
|
+
environmentPortalCapacity,
|
|
1078
|
+
environmentPortalMode,
|
|
1079
|
+
displayQuality: options.displayQuality === true,
|
|
1080
|
+
requiresMeshBvhForDisplayQuality: true,
|
|
1081
|
+
denoise: options.denoise !== false,
|
|
1082
|
+
frameIndex: readNonNegativeInteger("frameIndex", options.frameIndex, 0),
|
|
1083
|
+
memory: estimateWavefrontPathTracingMemory({
|
|
1084
|
+
tilePixelCapacity,
|
|
1085
|
+
sceneObjectCapacity,
|
|
1086
|
+
triangleCapacity,
|
|
1087
|
+
bvhNodeCapacity,
|
|
1088
|
+
bvhLeafSortCapacity,
|
|
1089
|
+
emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
|
|
1090
|
+
environmentPortalCapacity
|
|
1091
|
+
})
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function supportsWavefrontPathTracingCompute(options = {}) {
|
|
1095
|
+
const navigatorRef = options.navigator ?? globalThis.navigator;
|
|
1096
|
+
return typeof navigatorRef?.gpu?.requestAdapter === "function";
|
|
1097
|
+
}
|
|
1098
|
+
function getGpuUsageConstants() {
|
|
1099
|
+
if (typeof GPUBufferUsage === "undefined" || typeof GPUTextureUsage === "undefined" || typeof GPUShaderStage === "undefined") {
|
|
1100
|
+
throw new Error("WebGPU runtime unavailable. Required GPU constants are missing.");
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
buffer: GPUBufferUsage,
|
|
1104
|
+
texture: GPUTextureUsage,
|
|
1105
|
+
shader: GPUShaderStage,
|
|
1106
|
+
map: typeof GPUMapMode === "undefined" ? null : GPUMapMode
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function resolveCanvas(canvasOrSelector, documentRef = globalThis.document) {
|
|
1110
|
+
if (typeof canvasOrSelector === "string") {
|
|
1111
|
+
const resolved = documentRef?.querySelector?.(canvasOrSelector);
|
|
1112
|
+
if (!resolved) {
|
|
1113
|
+
throw new Error(`Unable to find canvas for selector: ${canvasOrSelector}`);
|
|
1114
|
+
}
|
|
1115
|
+
return resolved;
|
|
1116
|
+
}
|
|
1117
|
+
if (canvasOrSelector?.getContext) {
|
|
1118
|
+
return canvasOrSelector;
|
|
1119
|
+
}
|
|
1120
|
+
const fallback = documentRef?.querySelector?.("canvas[data-plasius-wavefront-path-tracing]");
|
|
1121
|
+
if (!fallback) {
|
|
1122
|
+
throw new Error("A canvas is required for WebGPU wavefront path tracing.");
|
|
1123
|
+
}
|
|
1124
|
+
return fallback;
|
|
1125
|
+
}
|
|
1126
|
+
function writeVec4(floatView, byteOffset, value) {
|
|
1127
|
+
const index = byteOffset / 4;
|
|
1128
|
+
floatView[index] = value[0] ?? 0;
|
|
1129
|
+
floatView[index + 1] = value[1] ?? 0;
|
|
1130
|
+
floatView[index + 2] = value[2] ?? 0;
|
|
1131
|
+
floatView[index + 3] = value[3] ?? 0;
|
|
1132
|
+
}
|
|
1133
|
+
function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length) {
|
|
1134
|
+
const normalized = Array.isArray(sceneObjects) && sceneObjects.length === 0 ? [] : normalizeSceneObjects(sceneObjects);
|
|
1135
|
+
if (normalized.length > capacity) {
|
|
1136
|
+
throw new Error(
|
|
1137
|
+
`Scene object capacity ${capacity} is too small for ${normalized.length} objects.`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
const bytes = new ArrayBuffer(Math.max(1, capacity) * SCENE_OBJECT_RECORD_BYTES);
|
|
1141
|
+
const uintView = new Uint32Array(bytes);
|
|
1142
|
+
const floatView = new Float32Array(bytes);
|
|
1143
|
+
normalized.forEach((object, index) => {
|
|
1144
|
+
const byteOffset = index * SCENE_OBJECT_RECORD_BYTES;
|
|
1145
|
+
const u32 = byteOffset / 4;
|
|
1146
|
+
uintView[u32] = object.kind;
|
|
1147
|
+
uintView[u32 + 1] = object.id;
|
|
1148
|
+
uintView[u32 + 2] = object.materialKind;
|
|
1149
|
+
uintView[u32 + 3] = object.flags;
|
|
1150
|
+
writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
|
|
1151
|
+
writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
|
|
1152
|
+
writeVec4(floatView, byteOffset + 48, object.color);
|
|
1153
|
+
writeVec4(floatView, byteOffset + 64, object.emission);
|
|
1154
|
+
writeVec4(floatView, byteOffset + 80, [
|
|
1155
|
+
object.roughness,
|
|
1156
|
+
object.metallic,
|
|
1157
|
+
object.opacity,
|
|
1158
|
+
object.ior
|
|
1159
|
+
]);
|
|
1160
|
+
});
|
|
1161
|
+
return Object.freeze({
|
|
1162
|
+
buffer: bytes,
|
|
1163
|
+
objects: Object.freeze(normalized),
|
|
1164
|
+
count: normalized.length,
|
|
1165
|
+
capacity
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
function packWavefrontTriangles(triangles, capacity = triangles.length) {
|
|
1169
|
+
if (triangles.length > capacity) {
|
|
1170
|
+
throw new Error(`Triangle capacity ${capacity} is too small for ${triangles.length} triangles.`);
|
|
1171
|
+
}
|
|
1172
|
+
const bytes = new ArrayBuffer(Math.max(1, capacity) * TRIANGLE_RECORD_BYTES);
|
|
1173
|
+
const uintView = new Uint32Array(bytes);
|
|
1174
|
+
const floatView = new Float32Array(bytes);
|
|
1175
|
+
triangles.forEach((triangle, index) => {
|
|
1176
|
+
const byteOffset = index * TRIANGLE_RECORD_BYTES;
|
|
1177
|
+
const u32 = byteOffset / 4;
|
|
1178
|
+
uintView[u32] = triangle.triangleId;
|
|
1179
|
+
uintView[u32 + 1] = triangle.meshId;
|
|
1180
|
+
uintView[u32 + 2] = triangle.materialKind;
|
|
1181
|
+
uintView[u32 + 3] = triangle.flags;
|
|
1182
|
+
uintView[u32 + 4] = triangle.materialRefId;
|
|
1183
|
+
uintView[u32 + 5] = triangle.mediumRefId;
|
|
1184
|
+
uintView[u32 + 6] = 0;
|
|
1185
|
+
uintView[u32 + 7] = 0;
|
|
1186
|
+
writeVec4(floatView, byteOffset + 32, [...triangle.v0, 0]);
|
|
1187
|
+
writeVec4(floatView, byteOffset + 48, [...triangle.v1, 0]);
|
|
1188
|
+
writeVec4(floatView, byteOffset + 64, [...triangle.v2, 0]);
|
|
1189
|
+
writeVec4(floatView, byteOffset + 80, [...triangle.n0, 0]);
|
|
1190
|
+
writeVec4(floatView, byteOffset + 96, [...triangle.n1, 0]);
|
|
1191
|
+
writeVec4(floatView, byteOffset + 112, [...triangle.n2, 0]);
|
|
1192
|
+
writeVec4(floatView, byteOffset + 128, [...triangle.uv0, ...triangle.uv1]);
|
|
1193
|
+
writeVec4(floatView, byteOffset + 144, [...triangle.uv2, 0, 0]);
|
|
1194
|
+
writeVec4(floatView, byteOffset + 160, triangle.color);
|
|
1195
|
+
writeVec4(floatView, byteOffset + 176, triangle.emission);
|
|
1196
|
+
writeVec4(floatView, byteOffset + 192, triangle.material);
|
|
1197
|
+
});
|
|
1198
|
+
return Object.freeze({
|
|
1199
|
+
buffer: bytes,
|
|
1200
|
+
triangles: Object.freeze(triangles),
|
|
1201
|
+
count: triangles.length,
|
|
1202
|
+
capacity
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
function packWavefrontBvhNodes(nodes, capacity = nodes.length) {
|
|
1206
|
+
if (nodes.length > capacity) {
|
|
1207
|
+
throw new Error(`BVH node capacity ${capacity} is too small for ${nodes.length} nodes.`);
|
|
1208
|
+
}
|
|
1209
|
+
const bytes = new ArrayBuffer(Math.max(1, capacity) * BVH_NODE_RECORD_BYTES);
|
|
1210
|
+
const uintView = new Uint32Array(bytes);
|
|
1211
|
+
const floatView = new Float32Array(bytes);
|
|
1212
|
+
nodes.forEach((node, index) => {
|
|
1213
|
+
const byteOffset = index * BVH_NODE_RECORD_BYTES;
|
|
1214
|
+
const u32 = byteOffset / 4;
|
|
1215
|
+
writeVec4(floatView, byteOffset, [...node.bounds.min, 0]);
|
|
1216
|
+
writeVec4(floatView, byteOffset + 16, [...node.bounds.max, 0]);
|
|
1217
|
+
uintView[u32 + 8] = node.triangleCount > 0 ? node.firstTriangle : node.leftChild;
|
|
1218
|
+
uintView[u32 + 9] = node.triangleCount;
|
|
1219
|
+
uintView[u32 + 10] = node.rightChild;
|
|
1220
|
+
uintView[u32 + 11] = 0;
|
|
1221
|
+
});
|
|
1222
|
+
return Object.freeze({
|
|
1223
|
+
buffer: bytes,
|
|
1224
|
+
nodes: Object.freeze(nodes),
|
|
1225
|
+
count: nodes.length,
|
|
1226
|
+
capacity
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
|
|
1230
|
+
const bytes = new ArrayBuffer(CONFIG_BUFFER_BYTES);
|
|
1231
|
+
const data = new DataView(bytes);
|
|
1232
|
+
const floatView = new Float32Array(bytes);
|
|
1233
|
+
const sampleIndex = buildRange.sampleIndex ?? 0;
|
|
1234
|
+
const sampleWeight = buildRange.sampleWeight ?? 1;
|
|
1235
|
+
data.setUint32(0, config.width, true);
|
|
1236
|
+
data.setUint32(4, config.height, true);
|
|
1237
|
+
data.setUint32(8, tile.x, true);
|
|
1238
|
+
data.setUint32(12, tile.y, true);
|
|
1239
|
+
data.setUint32(16, tile.width, true);
|
|
1240
|
+
data.setUint32(20, tile.height, true);
|
|
1241
|
+
data.setUint32(24, tile.width * tile.height, true);
|
|
1242
|
+
data.setUint32(28, config.maxDepth, true);
|
|
1243
|
+
data.setUint32(32, config.sceneObjectCount, true);
|
|
1244
|
+
data.setUint32(36, frameIndex, true);
|
|
1245
|
+
data.setUint32(40, config.denoise ? 1 : 0, true);
|
|
1246
|
+
data.setUint32(44, config.triangleCount, true);
|
|
1247
|
+
data.setUint32(48, config.bvhNodeCount, true);
|
|
1248
|
+
data.setUint32(52, config.displayQuality ? 1 : 0, true);
|
|
1249
|
+
data.setUint32(56, config.gpuMeshSource.meshes.count, true);
|
|
1250
|
+
data.setUint32(60, config.bvhNodeCapacity, true);
|
|
1251
|
+
writeVec4(floatView, 64, [...config.camera.position, 1]);
|
|
1252
|
+
writeVec4(floatView, 80, [...config.camera.forward, 0]);
|
|
1253
|
+
writeVec4(floatView, 96, [...config.camera.right, 0]);
|
|
1254
|
+
writeVec4(floatView, 112, [...config.camera.up, 0]);
|
|
1255
|
+
writeVec4(floatView, 128, [
|
|
1256
|
+
config.camera.tanHalfFovY,
|
|
1257
|
+
config.camera.aspect,
|
|
1258
|
+
sampleWeight,
|
|
1259
|
+
sampleIndex
|
|
1260
|
+
]);
|
|
1261
|
+
writeVec4(floatView, 144, config.environmentColor);
|
|
1262
|
+
writeVec4(floatView, 160, config.ambientColor);
|
|
1263
|
+
writeVec4(floatView, 176, config.environmentLighting.horizonColor);
|
|
1264
|
+
writeVec4(floatView, 192, config.environmentLighting.zenithColor);
|
|
1265
|
+
writeVec4(floatView, 208, [
|
|
1266
|
+
...config.environmentLighting.sunDirection,
|
|
1267
|
+
config.environmentLighting.intensity
|
|
1268
|
+
]);
|
|
1269
|
+
writeVec4(floatView, 224, config.environmentLighting.sunColor);
|
|
1270
|
+
data.setUint32(240, buildRange.start ?? 0, true);
|
|
1271
|
+
data.setUint32(244, buildRange.count ?? 0, true);
|
|
1272
|
+
data.setUint32(248, buildRange.sortItemCount ?? 0, true);
|
|
1273
|
+
data.setUint32(252, config.emissiveTriangleCount ?? 0, true);
|
|
1274
|
+
data.setUint32(256, config.environmentPortalCount ?? 0, true);
|
|
1275
|
+
data.setUint32(260, config.environmentPortalMode ?? 0, true);
|
|
1276
|
+
data.setUint32(264, 0, true);
|
|
1277
|
+
data.setUint32(268, 0, true);
|
|
1278
|
+
return bytes;
|
|
1279
|
+
}
|
|
1280
|
+
function createTiles(width, height, tileSize) {
|
|
1281
|
+
const tiles = [];
|
|
1282
|
+
for (let y = 0; y < height; y += tileSize) {
|
|
1283
|
+
for (let x = 0; x < width; x += tileSize) {
|
|
1284
|
+
tiles.push(
|
|
1285
|
+
Object.freeze({
|
|
1286
|
+
x,
|
|
1287
|
+
y,
|
|
1288
|
+
width: Math.min(tileSize, width - x),
|
|
1289
|
+
height: Math.min(tileSize, height - y)
|
|
1290
|
+
})
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return Object.freeze(tiles);
|
|
1295
|
+
}
|
|
1296
|
+
function clampTileSizeForDevice(config, device) {
|
|
1297
|
+
const limit = Number(device?.limits?.maxStorageBufferBindingSize);
|
|
1298
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
1299
|
+
return config.tileSize;
|
|
1300
|
+
}
|
|
1301
|
+
const maxPixelsByRay = Math.floor(limit / RAY_RECORD_BYTES);
|
|
1302
|
+
const maxPixelsByHit = Math.floor(limit / HIT_RECORD_BYTES);
|
|
1303
|
+
const maxPixels = Math.max(256, Math.min(maxPixelsByRay, maxPixelsByHit));
|
|
1304
|
+
if (config.tilePixelCapacity <= maxPixels) {
|
|
1305
|
+
return config.tileSize;
|
|
1306
|
+
}
|
|
1307
|
+
return Math.max(16, Math.floor(Math.sqrt(maxPixels)));
|
|
1308
|
+
}
|
|
1309
|
+
function createBuffer(device, usage, size, label) {
|
|
1310
|
+
return device.createBuffer({
|
|
1311
|
+
label,
|
|
1312
|
+
size: Math.max(4, size),
|
|
1313
|
+
usage
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
function alignTo(value, alignment) {
|
|
1317
|
+
const resolvedAlignment = Math.max(1, alignment);
|
|
1318
|
+
return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
|
|
1319
|
+
}
|
|
1320
|
+
async function getPipelineDiagnostics(shaderModule) {
|
|
1321
|
+
if (typeof shaderModule?.compilationInfo !== "function") {
|
|
1322
|
+
return "";
|
|
1323
|
+
}
|
|
1324
|
+
try {
|
|
1325
|
+
const info = await shaderModule.compilationInfo();
|
|
1326
|
+
const messages = info.messages ?? [];
|
|
1327
|
+
if (messages.length === 0) {
|
|
1328
|
+
return "";
|
|
1329
|
+
}
|
|
1330
|
+
return messages.map((message) => {
|
|
1331
|
+
const line = message.lineNum ?? "?";
|
|
1332
|
+
const column = message.linePos ?? "?";
|
|
1333
|
+
return `line ${line}:${column} ${message.message}`;
|
|
1334
|
+
}).join("\n");
|
|
1335
|
+
} catch {
|
|
1336
|
+
return "";
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
async function createComputePipeline(device, shaderModule, layout, entryPoint, label) {
|
|
1340
|
+
const descriptor = {
|
|
1341
|
+
label,
|
|
1342
|
+
layout,
|
|
1343
|
+
compute: {
|
|
1344
|
+
module: shaderModule,
|
|
1345
|
+
entryPoint
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
try {
|
|
1349
|
+
if (typeof device.createComputePipelineAsync === "function") {
|
|
1350
|
+
return await device.createComputePipelineAsync(descriptor);
|
|
1351
|
+
}
|
|
1352
|
+
return device.createComputePipeline(descriptor);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
const diagnostics = await getPipelineDiagnostics(shaderModule);
|
|
1355
|
+
const suffix = diagnostics ? `
|
|
1356
|
+
${diagnostics}` : "";
|
|
1357
|
+
throw new Error(`WGSL compilation failed for ${label}: ${error.message}${suffix}`, {
|
|
1358
|
+
cause: error
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function createRenderPipeline(device, descriptor) {
|
|
1363
|
+
if (typeof device.createRenderPipelineAsync === "function") {
|
|
1364
|
+
return device.createRenderPipelineAsync(descriptor);
|
|
1365
|
+
}
|
|
1366
|
+
return device.createRenderPipeline(descriptor);
|
|
1367
|
+
}
|
|
1368
|
+
var WAVEFRONT_COMPUTE_WGSL = `
|
|
1369
|
+
const RAY_FLAG_GUIDED_EMISSIVE: u32 = 1u;
|
|
1370
|
+
|
|
1371
|
+
struct RayRecord {
|
|
1372
|
+
rayId: u32,
|
|
1373
|
+
parentRayId: u32,
|
|
1374
|
+
sourcePixelId: u32,
|
|
1375
|
+
sampleId: u32,
|
|
1376
|
+
bounce: u32,
|
|
1377
|
+
mediumRefId: u32,
|
|
1378
|
+
flags: u32,
|
|
1379
|
+
pad0: u32,
|
|
1380
|
+
origin: vec4<f32>,
|
|
1381
|
+
direction: vec4<f32>,
|
|
1382
|
+
throughput: vec4<f32>,
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
struct HitRecord {
|
|
1386
|
+
rayId: u32,
|
|
1387
|
+
sourcePixelId: u32,
|
|
1388
|
+
hitType: u32,
|
|
1389
|
+
objectId: u32,
|
|
1390
|
+
materialKind: u32,
|
|
1391
|
+
frontFace: u32,
|
|
1392
|
+
primitiveId: u32,
|
|
1393
|
+
materialRefId: u32,
|
|
1394
|
+
mediumRefId: u32,
|
|
1395
|
+
pad0: u32,
|
|
1396
|
+
pad1: u32,
|
|
1397
|
+
pad2: u32,
|
|
1398
|
+
distance: f32,
|
|
1399
|
+
pad3: vec3<f32>,
|
|
1400
|
+
position: vec4<f32>,
|
|
1401
|
+
geometricNormal: vec4<f32>,
|
|
1402
|
+
shadingNormal: vec4<f32>,
|
|
1403
|
+
barycentric: vec4<f32>,
|
|
1404
|
+
uv: vec4<f32>,
|
|
1405
|
+
color: vec4<f32>,
|
|
1406
|
+
emission: vec4<f32>,
|
|
1407
|
+
material: vec4<f32>,
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
struct SceneObject {
|
|
1411
|
+
kind: u32,
|
|
1412
|
+
objectId: u32,
|
|
1413
|
+
materialKind: u32,
|
|
1414
|
+
flags: u32,
|
|
1415
|
+
center: vec4<f32>,
|
|
1416
|
+
halfExtent: vec4<f32>,
|
|
1417
|
+
color: vec4<f32>,
|
|
1418
|
+
emission: vec4<f32>,
|
|
1419
|
+
material: vec4<f32>,
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
struct TriangleRecord {
|
|
1423
|
+
triangleId: u32,
|
|
1424
|
+
meshId: u32,
|
|
1425
|
+
materialKind: u32,
|
|
1426
|
+
flags: u32,
|
|
1427
|
+
materialRefId: u32,
|
|
1428
|
+
mediumRefId: u32,
|
|
1429
|
+
pad0: u32,
|
|
1430
|
+
pad1: u32,
|
|
1431
|
+
v0: vec4<f32>,
|
|
1432
|
+
v1: vec4<f32>,
|
|
1433
|
+
v2: vec4<f32>,
|
|
1434
|
+
n0: vec4<f32>,
|
|
1435
|
+
n1: vec4<f32>,
|
|
1436
|
+
n2: vec4<f32>,
|
|
1437
|
+
uv0uv1: vec4<f32>,
|
|
1438
|
+
uv2Pad: vec4<f32>,
|
|
1439
|
+
color: vec4<f32>,
|
|
1440
|
+
emission: vec4<f32>,
|
|
1441
|
+
material: vec4<f32>,
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
struct BvhNode {
|
|
1445
|
+
boundsMin: vec4<f32>,
|
|
1446
|
+
boundsMax: vec4<f32>,
|
|
1447
|
+
childOrFirst: u32,
|
|
1448
|
+
triangleCount: u32,
|
|
1449
|
+
rightChild: u32,
|
|
1450
|
+
pad0: u32,
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
struct BvhLeafRef {
|
|
1454
|
+
key: u32,
|
|
1455
|
+
triangleIndex: u32,
|
|
1456
|
+
pad0: u32,
|
|
1457
|
+
pad1: u32,
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
struct ScatterResult {
|
|
1461
|
+
direction: vec4<f32>,
|
|
1462
|
+
flags: u32,
|
|
1463
|
+
pad0: u32,
|
|
1464
|
+
pad1: u32,
|
|
1465
|
+
pad2: u32,
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
struct MeshVertex {
|
|
1469
|
+
position: vec4<f32>,
|
|
1470
|
+
normal: vec4<f32>,
|
|
1471
|
+
uv: vec4<f32>,
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
struct MeshRange {
|
|
1475
|
+
meshId: u32,
|
|
1476
|
+
materialKind: u32,
|
|
1477
|
+
flags: u32,
|
|
1478
|
+
materialRefId: u32,
|
|
1479
|
+
mediumRefId: u32,
|
|
1480
|
+
firstIndex: u32,
|
|
1481
|
+
indexCount: u32,
|
|
1482
|
+
firstTriangle: u32,
|
|
1483
|
+
triangleCount: u32,
|
|
1484
|
+
firstVertex: u32,
|
|
1485
|
+
vertexCount: u32,
|
|
1486
|
+
pad0: u32,
|
|
1487
|
+
color: vec4<f32>,
|
|
1488
|
+
emission: vec4<f32>,
|
|
1489
|
+
material: vec4<f32>,
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
struct FrameConfig {
|
|
1493
|
+
canvasWidth: u32,
|
|
1494
|
+
canvasHeight: u32,
|
|
1495
|
+
tileX: u32,
|
|
1496
|
+
tileY: u32,
|
|
1497
|
+
tileWidth: u32,
|
|
1498
|
+
tileHeight: u32,
|
|
1499
|
+
tilePixelCount: u32,
|
|
1500
|
+
maxDepth: u32,
|
|
1501
|
+
sceneObjectCount: u32,
|
|
1502
|
+
frameIndex: u32,
|
|
1503
|
+
denoise: u32,
|
|
1504
|
+
triangleCount: u32,
|
|
1505
|
+
bvhNodeCount: u32,
|
|
1506
|
+
displayQuality: u32,
|
|
1507
|
+
meshSourceCount: u32,
|
|
1508
|
+
bvhNodeCapacity: u32,
|
|
1509
|
+
cameraPosition: vec4<f32>,
|
|
1510
|
+
cameraForward: vec4<f32>,
|
|
1511
|
+
cameraRight: vec4<f32>,
|
|
1512
|
+
cameraUp: vec4<f32>,
|
|
1513
|
+
projectionAndSampling: vec4<f32>,
|
|
1514
|
+
environmentColor: vec4<f32>,
|
|
1515
|
+
ambientColor: vec4<f32>,
|
|
1516
|
+
environmentHorizonColor: vec4<f32>,
|
|
1517
|
+
environmentZenithColor: vec4<f32>,
|
|
1518
|
+
environmentSunDirectionIntensity: vec4<f32>,
|
|
1519
|
+
environmentSunColor: vec4<f32>,
|
|
1520
|
+
bvhBuildNodeStart: u32,
|
|
1521
|
+
bvhBuildNodeCount: u32,
|
|
1522
|
+
bvhSortItemCount: u32,
|
|
1523
|
+
emissiveTriangleCount: u32,
|
|
1524
|
+
environmentPortalCount: u32,
|
|
1525
|
+
environmentPortalMode: u32,
|
|
1526
|
+
_portalPad0: u32,
|
|
1527
|
+
_portalPad1: u32,
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
struct Counters {
|
|
1531
|
+
activeCount: atomic<u32>,
|
|
1532
|
+
nextCount: atomic<u32>,
|
|
1533
|
+
terminatedCount: atomic<u32>,
|
|
1534
|
+
hitCount: atomic<u32>,
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
struct Candidate {
|
|
1538
|
+
hit: u32,
|
|
1539
|
+
distance: f32,
|
|
1540
|
+
geometricNormal: vec3<f32>,
|
|
1541
|
+
shadingNormal: vec3<f32>,
|
|
1542
|
+
barycentric: vec3<f32>,
|
|
1543
|
+
uv: vec2<f32>,
|
|
1544
|
+
frontFace: u32,
|
|
1545
|
+
triangleIndex: u32,
|
|
1546
|
+
primitiveId: u32,
|
|
1547
|
+
materialRefId: u32,
|
|
1548
|
+
mediumRefId: u32,
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
struct EnvironmentPortal {
|
|
1552
|
+
kind: u32,
|
|
1553
|
+
flags: u32,
|
|
1554
|
+
_pad0: u32,
|
|
1555
|
+
_pad1: u32,
|
|
1556
|
+
position: vec4<f32>,
|
|
1557
|
+
normal: vec4<f32>,
|
|
1558
|
+
tangent: vec4<f32>,
|
|
1559
|
+
bitangent: vec4<f32>,
|
|
1560
|
+
color: vec4<f32>,
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
@group(0) @binding(0) var<storage, read_write> activeQueue: array<RayRecord>;
|
|
1564
|
+
@group(0) @binding(1) var<storage, read_write> nextQueue: array<RayRecord>;
|
|
1565
|
+
@group(0) @binding(2) var<storage, read_write> hits: array<HitRecord>;
|
|
1566
|
+
@group(0) @binding(3) var<storage, read_write> accumulation: array<vec4<f32>>;
|
|
1567
|
+
@group(0) @binding(4) var<storage, read> sceneObjects: array<SceneObject>;
|
|
1568
|
+
@group(0) @binding(5) var<uniform> config: FrameConfig;
|
|
1569
|
+
@group(0) @binding(6) var<storage, read_write> counters: Counters;
|
|
1570
|
+
@group(0) @binding(7) var outputImage: texture_storage_2d<rgba8unorm, write>;
|
|
1571
|
+
@group(0) @binding(8) var<storage, read_write> triangles: array<TriangleRecord>;
|
|
1572
|
+
@group(0) @binding(9) var<storage, read_write> bvhNodes: array<BvhNode>;
|
|
1573
|
+
@group(0) @binding(10) var<storage, read> meshVertices: array<MeshVertex>;
|
|
1574
|
+
@group(0) @binding(11) var<storage, read> meshIndices: array<u32>;
|
|
1575
|
+
@group(0) @binding(12) var<storage, read> meshRanges: array<MeshRange>;
|
|
1576
|
+
@group(0) @binding(13) var<storage, read_write> bvhLeafRefs: array<BvhLeafRef>;
|
|
1577
|
+
@group(0) @binding(14) var denoiseInputRadiance: texture_2d<f32>;
|
|
1578
|
+
@group(0) @binding(15) var denoisedRadianceImage: texture_storage_2d<rgba16float, write>;
|
|
1579
|
+
@group(0) @binding(16) var radianceImage: texture_storage_2d<rgba16float, write>;
|
|
1580
|
+
@group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
|
|
1581
|
+
@group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
|
|
1582
|
+
@group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
|
|
1583
|
+
|
|
1584
|
+
fn hash_u32(value: u32) -> u32 {
|
|
1585
|
+
var x = value;
|
|
1586
|
+
x = ((x >> 16u) ^ x) * 0x45d9f3bu;
|
|
1587
|
+
x = ((x >> 16u) ^ x) * 0x45d9f3bu;
|
|
1588
|
+
x = (x >> 16u) ^ x;
|
|
1589
|
+
return x;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
fn mix_seed(pixelId: u32, sampleId: u32, bounce: u32, frameIndex: u32, dimension: u32) -> u32 {
|
|
1593
|
+
var x =
|
|
1594
|
+
(pixelId * 747796405u) ^
|
|
1595
|
+
(sampleId * 2891336453u) ^
|
|
1596
|
+
(bounce * 277803737u) ^
|
|
1597
|
+
(frameIndex * 1442695041u) ^
|
|
1598
|
+
(dimension * 1597334677u);
|
|
1599
|
+
x = x ^ (x >> 16u);
|
|
1600
|
+
x = x * 0x7feb352du;
|
|
1601
|
+
x = x ^ (x >> 15u);
|
|
1602
|
+
x = x * 0x846ca68bu;
|
|
1603
|
+
x = x ^ (x >> 16u);
|
|
1604
|
+
return x;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
fn random01(seed: u32) -> f32 {
|
|
1608
|
+
return f32(hash_u32(seed) & 0x00ffffffu) / 16777215.0;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
fn safe_normalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
|
|
1612
|
+
let len = length(value);
|
|
1613
|
+
if (len <= 0.000001) {
|
|
1614
|
+
return fallback;
|
|
1615
|
+
}
|
|
1616
|
+
return value / len;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
fn saturate(value: f32) -> f32 {
|
|
1620
|
+
return clamp(value, 0.0, 1.0);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
fn max_component(value: vec3<f32>) -> f32 {
|
|
1624
|
+
return max(max(value.x, value.y), value.z);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
|
|
1628
|
+
if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
|
|
1629
|
+
return vec3<f32>(1.0);
|
|
1630
|
+
}
|
|
1631
|
+
var scale = vec3<f32>(0.0);
|
|
1632
|
+
for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
|
|
1633
|
+
let portal = environmentPortals[portalIndex];
|
|
1634
|
+
if (portal.kind == 1u) {
|
|
1635
|
+
let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
|
|
1636
|
+
let denominator = dot(direction, portalNormal);
|
|
1637
|
+
let twoSided = (portal.flags & 1u) != 0u;
|
|
1638
|
+
var facing = abs(denominator) > 0.0001;
|
|
1639
|
+
if (!twoSided && denominator <= 0.0001) {
|
|
1640
|
+
facing = false;
|
|
1641
|
+
}
|
|
1642
|
+
if (facing) {
|
|
1643
|
+
let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
|
|
1644
|
+
if (distance > 0.001) {
|
|
1645
|
+
let hitPosition = origin + direction * distance;
|
|
1646
|
+
let local = hitPosition - portal.position.xyz;
|
|
1647
|
+
let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
|
|
1648
|
+
let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
|
|
1649
|
+
let u = dot(local, tangent);
|
|
1650
|
+
let v = dot(local, bitangent);
|
|
1651
|
+
if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
|
|
1652
|
+
let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
|
|
1653
|
+
let angleWeight = max(abs(denominator), 0.08);
|
|
1654
|
+
let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
|
|
1655
|
+
scale = max(scale, portalScale);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return scale;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
|
|
1665
|
+
let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
|
|
1666
|
+
let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
|
|
1667
|
+
let sunDirection = safe_normalize(
|
|
1668
|
+
config.environmentSunDirectionIntensity.xyz,
|
|
1669
|
+
vec3<f32>(0.0, 1.0, 0.0)
|
|
1670
|
+
);
|
|
1671
|
+
let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
|
|
1672
|
+
let gradient =
|
|
1673
|
+
config.environmentHorizonColor.xyz * (1.0 - upFactor) +
|
|
1674
|
+
config.environmentZenithColor.xyz * upFactor;
|
|
1675
|
+
let portalScale = environment_portal_radiance_scale(origin, rayDirection);
|
|
1676
|
+
let portalHit = max_component(portalScale) > 0.0001;
|
|
1677
|
+
return (
|
|
1678
|
+
gradient +
|
|
1679
|
+
config.environmentSunColor.xyz * sunGlow
|
|
1680
|
+
) *
|
|
1681
|
+
max(config.environmentSunDirectionIntensity.w, 0.0001) *
|
|
1682
|
+
select(vec3<f32>(1.0), portalScale, portalHit);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
|
|
1686
|
+
let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
|
|
1687
|
+
if (
|
|
1688
|
+
config.environmentPortalCount > 0u &&
|
|
1689
|
+
config.environmentPortalMode == 2u &&
|
|
1690
|
+
max_component(portalScale) <= 0.0001
|
|
1691
|
+
) {
|
|
1692
|
+
return config.ambientColor.xyz * 0.65;
|
|
1693
|
+
}
|
|
1694
|
+
return environment_radiance(origin, direction);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
fn default_mesh_range() -> MeshRange {
|
|
1698
|
+
return MeshRange(
|
|
1699
|
+
0u,
|
|
1700
|
+
0u,
|
|
1701
|
+
0u,
|
|
1702
|
+
0u,
|
|
1703
|
+
0u,
|
|
1704
|
+
0u,
|
|
1705
|
+
0u,
|
|
1706
|
+
0u,
|
|
1707
|
+
0u,
|
|
1708
|
+
0u,
|
|
1709
|
+
0u,
|
|
1710
|
+
0u,
|
|
1711
|
+
vec4<f32>(0.72, 0.72, 0.68, 1.0),
|
|
1712
|
+
vec4<f32>(0.0),
|
|
1713
|
+
vec4<f32>(0.72, 0.0, 1.0, 1.45)
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
fn mesh_range_for_triangle(triangleIndex: u32) -> MeshRange {
|
|
1718
|
+
var selected = default_mesh_range();
|
|
1719
|
+
for (var meshIndex = 0u; meshIndex < config.meshSourceCount; meshIndex = meshIndex + 1u) {
|
|
1720
|
+
let mesh = meshRanges[meshIndex];
|
|
1721
|
+
let triangleStart = mesh.firstTriangle;
|
|
1722
|
+
let triangleEnd = mesh.firstTriangle + mesh.triangleCount;
|
|
1723
|
+
if (triangleIndex >= triangleStart && triangleIndex < triangleEnd) {
|
|
1724
|
+
selected = mesh;
|
|
1725
|
+
break;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return selected;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
fn node_bounds_min(left: BvhNode, right: BvhNode) -> vec3<f32> {
|
|
1732
|
+
return min(left.boundsMin.xyz, right.boundsMin.xyz);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
fn node_bounds_max(left: BvhNode, right: BvhNode) -> vec3<f32> {
|
|
1736
|
+
return max(left.boundsMax.xyz, right.boundsMax.xyz);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
fn ordered_float_key(value: f32) -> u32 {
|
|
1740
|
+
let bits = bitcast<u32>(value);
|
|
1741
|
+
let sign = bits & 0x80000000u;
|
|
1742
|
+
let mask = select(0x80000000u, 0xffffffffu, sign != 0u);
|
|
1743
|
+
return bits ^ mask;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
fn split_by_3(value: u32) -> u32 {
|
|
1747
|
+
var x = value & 0x000003ffu;
|
|
1748
|
+
x = (x | (x << 16u)) & 0x030000ffu;
|
|
1749
|
+
x = (x | (x << 8u)) & 0x0300f00fu;
|
|
1750
|
+
x = (x | (x << 4u)) & 0x030c30c3u;
|
|
1751
|
+
x = (x | (x << 2u)) & 0x09249249u;
|
|
1752
|
+
return x;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
fn morton_key_from_centroid(centroid: vec3<f32>) -> u32 {
|
|
1756
|
+
let x = (ordered_float_key(centroid.x) >> 12u) & 0x000003ffu;
|
|
1757
|
+
let y = (ordered_float_key(centroid.y) >> 12u) & 0x000003ffu;
|
|
1758
|
+
let z = (ordered_float_key(centroid.z) >> 12u) & 0x000003ffu;
|
|
1759
|
+
return (split_by_3(x) << 2u) | (split_by_3(y) << 1u) | split_by_3(z);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
fn leaf_ref_less(left: BvhLeafRef, right: BvhLeafRef) -> bool {
|
|
1763
|
+
if (left.key < right.key) {
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
if (left.key > right.key) {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
return left.triangleIndex < right.triangleIndex;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
@compute @workgroup_size(64)
|
|
1773
|
+
fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
1774
|
+
let triangleIndex = globalId.x;
|
|
1775
|
+
if (triangleIndex >= config.triangleCount) {
|
|
1776
|
+
if (triangleIndex < config.bvhSortItemCount) {
|
|
1777
|
+
bvhLeafRefs[triangleIndex] = BvhLeafRef(0xffffffffu, 0xffffffffu, 0u, 0u);
|
|
1778
|
+
}
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
let mesh = mesh_range_for_triangle(triangleIndex);
|
|
1783
|
+
let localTriangle = triangleIndex - mesh.firstTriangle;
|
|
1784
|
+
let indexOffset = mesh.firstIndex + localTriangle * 3u;
|
|
1785
|
+
let index0 = meshIndices[indexOffset];
|
|
1786
|
+
let index1 = meshIndices[indexOffset + 1u];
|
|
1787
|
+
let index2 = meshIndices[indexOffset + 2u];
|
|
1788
|
+
let vertex0 = meshVertices[index0];
|
|
1789
|
+
let vertex1 = meshVertices[index1];
|
|
1790
|
+
let vertex2 = meshVertices[index2];
|
|
1791
|
+
let edge1 = vertex1.position.xyz - vertex0.position.xyz;
|
|
1792
|
+
let edge2 = vertex2.position.xyz - vertex0.position.xyz;
|
|
1793
|
+
let centroid = (vertex0.position.xyz + vertex1.position.xyz + vertex2.position.xyz) / 3.0;
|
|
1794
|
+
let faceNormal = safe_normalize(cross(edge1, edge2), vec3<f32>(0.0, 1.0, 0.0));
|
|
1795
|
+
let n0 = select(faceNormal, safe_normalize(vertex0.normal.xyz, faceNormal), vertex0.normal.w > 0.5);
|
|
1796
|
+
let n1 = select(faceNormal, safe_normalize(vertex1.normal.xyz, faceNormal), vertex1.normal.w > 0.5);
|
|
1797
|
+
let n2 = select(faceNormal, safe_normalize(vertex2.normal.xyz, faceNormal), vertex2.normal.w > 0.5);
|
|
1798
|
+
let uv0 = select(vec2<f32>(0.0), vertex0.uv.xy, vertex0.uv.z > 0.5);
|
|
1799
|
+
let uv1 = select(vec2<f32>(0.0), vertex1.uv.xy, vertex1.uv.z > 0.5);
|
|
1800
|
+
let uv2 = select(vec2<f32>(0.0), vertex2.uv.xy, vertex2.uv.z > 0.5);
|
|
1801
|
+
|
|
1802
|
+
triangles[triangleIndex] = TriangleRecord(
|
|
1803
|
+
triangleIndex,
|
|
1804
|
+
mesh.meshId,
|
|
1805
|
+
mesh.materialKind,
|
|
1806
|
+
mesh.flags,
|
|
1807
|
+
mesh.materialRefId,
|
|
1808
|
+
mesh.mediumRefId,
|
|
1809
|
+
0u,
|
|
1810
|
+
0u,
|
|
1811
|
+
vec4<f32>(vertex0.position.xyz, 0.0),
|
|
1812
|
+
vec4<f32>(vertex1.position.xyz, 0.0),
|
|
1813
|
+
vec4<f32>(vertex2.position.xyz, 0.0),
|
|
1814
|
+
vec4<f32>(n0, 0.0),
|
|
1815
|
+
vec4<f32>(n1, 0.0),
|
|
1816
|
+
vec4<f32>(n2, 0.0),
|
|
1817
|
+
vec4<f32>(uv0, uv1),
|
|
1818
|
+
vec4<f32>(uv2, 0.0, 0.0),
|
|
1819
|
+
mesh.color,
|
|
1820
|
+
mesh.emission,
|
|
1821
|
+
mesh.material
|
|
1822
|
+
);
|
|
1823
|
+
|
|
1824
|
+
let leafBase = config.triangleCount - 1u;
|
|
1825
|
+
let nodeIndex = leafBase + triangleIndex;
|
|
1826
|
+
let boundsMin = min(vertex0.position.xyz, min(vertex1.position.xyz, vertex2.position.xyz));
|
|
1827
|
+
let boundsMax = max(vertex0.position.xyz, max(vertex1.position.xyz, vertex2.position.xyz));
|
|
1828
|
+
bvhLeafRefs[triangleIndex] = BvhLeafRef(
|
|
1829
|
+
morton_key_from_centroid(centroid),
|
|
1830
|
+
triangleIndex,
|
|
1831
|
+
0u,
|
|
1832
|
+
0u
|
|
1833
|
+
);
|
|
1834
|
+
bvhNodes[nodeIndex] = BvhNode(
|
|
1835
|
+
vec4<f32>(boundsMin, 0.0),
|
|
1836
|
+
vec4<f32>(boundsMax, 0.0),
|
|
1837
|
+
triangleIndex,
|
|
1838
|
+
1u,
|
|
1839
|
+
0u,
|
|
1840
|
+
0u
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
@compute @workgroup_size(64)
|
|
1845
|
+
fn sortBvhLeafRefs(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
1846
|
+
let index = globalId.x;
|
|
1847
|
+
let sortCount = config.bvhSortItemCount;
|
|
1848
|
+
if (sortCount <= 1u || index >= sortCount) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
let compareDistance = config.bvhBuildNodeStart;
|
|
1853
|
+
let sequenceSize = config.bvhBuildNodeCount;
|
|
1854
|
+
if (compareDistance == 0u || sequenceSize == 0u) {
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
let partner = index ^ compareDistance;
|
|
1859
|
+
if (partner <= index || partner >= sortCount) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
let left = bvhLeafRefs[index];
|
|
1864
|
+
let right = bvhLeafRefs[partner];
|
|
1865
|
+
let ascending = (index & sequenceSize) == 0u;
|
|
1866
|
+
let leftIsLess = leaf_ref_less(left, right);
|
|
1867
|
+
let rightIsLess = leaf_ref_less(right, left);
|
|
1868
|
+
let shouldSwap = select(leftIsLess, rightIsLess, ascending);
|
|
1869
|
+
if (shouldSwap) {
|
|
1870
|
+
bvhLeafRefs[index] = right;
|
|
1871
|
+
bvhLeafRefs[partner] = left;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
@compute @workgroup_size(64)
|
|
1876
|
+
fn writeSortedBvhLeaves(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
1877
|
+
let sortedIndex = globalId.x;
|
|
1878
|
+
if (sortedIndex >= config.triangleCount || config.triangleCount == 0u) {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
let leafRef = bvhLeafRefs[sortedIndex];
|
|
1883
|
+
if (leafRef.triangleIndex >= config.triangleCount) {
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
let triangle = triangles[leafRef.triangleIndex];
|
|
1888
|
+
let boundsMin = min(triangle.v0.xyz, min(triangle.v1.xyz, triangle.v2.xyz));
|
|
1889
|
+
let boundsMax = max(triangle.v0.xyz, max(triangle.v1.xyz, triangle.v2.xyz));
|
|
1890
|
+
let leafBase = config.triangleCount - 1u;
|
|
1891
|
+
let nodeIndex = leafBase + sortedIndex;
|
|
1892
|
+
bvhNodes[nodeIndex] = BvhNode(
|
|
1893
|
+
vec4<f32>(boundsMin, 0.0),
|
|
1894
|
+
vec4<f32>(boundsMax, 0.0),
|
|
1895
|
+
leafRef.triangleIndex,
|
|
1896
|
+
1u,
|
|
1897
|
+
0u,
|
|
1898
|
+
0u
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
@compute @workgroup_size(64)
|
|
1903
|
+
fn buildBvhInternalLevel(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
1904
|
+
if (config.triangleCount <= 1u || globalId.x >= config.bvhBuildNodeCount) {
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
let internalCount = config.triangleCount - 1u;
|
|
1909
|
+
let nodeIndex = config.bvhBuildNodeStart + globalId.x;
|
|
1910
|
+
if (nodeIndex >= internalCount || nodeIndex >= config.bvhNodeCapacity) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
let leftIndex = nodeIndex * 2u + 1u;
|
|
1915
|
+
let rightIndex = nodeIndex * 2u + 2u;
|
|
1916
|
+
if (rightIndex >= config.bvhNodeCapacity || rightIndex >= config.bvhNodeCount) {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
let left = bvhNodes[leftIndex];
|
|
1921
|
+
let right = bvhNodes[rightIndex];
|
|
1922
|
+
bvhNodes[nodeIndex] = BvhNode(
|
|
1923
|
+
vec4<f32>(node_bounds_min(left, right), 0.0),
|
|
1924
|
+
vec4<f32>(node_bounds_max(left, right), 0.0),
|
|
1925
|
+
leftIndex,
|
|
1926
|
+
0u,
|
|
1927
|
+
rightIndex,
|
|
1928
|
+
0u
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
fn make_ray(pixelIndex: u32) -> RayRecord {
|
|
1933
|
+
let localX = pixelIndex % config.tileWidth;
|
|
1934
|
+
let localY = pixelIndex / config.tileWidth;
|
|
1935
|
+
let px = config.tileX + localX;
|
|
1936
|
+
let py = config.tileY + localY;
|
|
1937
|
+
let sampleId = u32(config.projectionAndSampling.w);
|
|
1938
|
+
let sourcePixelId = py * config.canvasWidth + px;
|
|
1939
|
+
let jitterX = random01(mix_seed(sourcePixelId, sampleId, 0u, config.frameIndex, 1u)) - 0.5;
|
|
1940
|
+
let jitterY = random01(mix_seed(sourcePixelId, sampleId, 0u, config.frameIndex, 2u)) - 0.5;
|
|
1941
|
+
let ndcX = ((f32(px) + 0.5 + jitterX * 0.35) / f32(config.canvasWidth)) * 2.0 - 1.0;
|
|
1942
|
+
let ndcY = 1.0 - ((f32(py) + 0.5 + jitterY * 0.35) / f32(config.canvasHeight)) * 2.0;
|
|
1943
|
+
let viewX = ndcX * config.projectionAndSampling.x * config.projectionAndSampling.y;
|
|
1944
|
+
let viewY = ndcY * config.projectionAndSampling.x;
|
|
1945
|
+
let direction = safe_normalize(
|
|
1946
|
+
config.cameraForward.xyz + config.cameraRight.xyz * viewX + config.cameraUp.xyz * viewY,
|
|
1947
|
+
config.cameraForward.xyz
|
|
1948
|
+
);
|
|
1949
|
+
return RayRecord(
|
|
1950
|
+
pixelIndex,
|
|
1951
|
+
0xffffffffu,
|
|
1952
|
+
sourcePixelId,
|
|
1953
|
+
sampleId,
|
|
1954
|
+
0u,
|
|
1955
|
+
0u,
|
|
1956
|
+
0u,
|
|
1957
|
+
0u,
|
|
1958
|
+
vec4<f32>(config.cameraPosition.xyz, 1.0),
|
|
1959
|
+
vec4<f32>(direction, 0.0),
|
|
1960
|
+
vec4<f32>(1.0, 1.0, 1.0, 1.0)
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
fn make_miss(ray: RayRecord) -> HitRecord {
|
|
1965
|
+
let radiance = gated_environment_radiance(ray.origin.xyz, ray.direction.xyz);
|
|
1966
|
+
return HitRecord(
|
|
1967
|
+
ray.rayId,
|
|
1968
|
+
ray.sourcePixelId,
|
|
1969
|
+
2u,
|
|
1970
|
+
0u,
|
|
1971
|
+
0u,
|
|
1972
|
+
1u,
|
|
1973
|
+
0u,
|
|
1974
|
+
0u,
|
|
1975
|
+
0u,
|
|
1976
|
+
0u,
|
|
1977
|
+
0u,
|
|
1978
|
+
0u,
|
|
1979
|
+
-1.0,
|
|
1980
|
+
vec3<f32>(0.0),
|
|
1981
|
+
vec4<f32>(ray.origin.xyz + ray.direction.xyz * 1000.0, 1.0),
|
|
1982
|
+
vec4<f32>(-ray.direction.xyz, 0.0),
|
|
1983
|
+
vec4<f32>(-ray.direction.xyz, 0.0),
|
|
1984
|
+
vec4<f32>(0.0),
|
|
1985
|
+
vec4<f32>(0.0),
|
|
1986
|
+
vec4<f32>(radiance, 1.0),
|
|
1987
|
+
vec4<f32>(0.0),
|
|
1988
|
+
vec4<f32>(1.0, 0.0, 1.0, 1.0)
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
|
|
1993
|
+
let oc = ray.origin.xyz - object.center.xyz;
|
|
1994
|
+
let radius = max(object.halfExtent.x, 0.001);
|
|
1995
|
+
let halfB = dot(oc, ray.direction.xyz);
|
|
1996
|
+
let c = dot(oc, oc) - radius * radius;
|
|
1997
|
+
let discriminant = halfB * halfB - c;
|
|
1998
|
+
if (discriminant < 0.0) {
|
|
1999
|
+
return no_candidate();
|
|
2000
|
+
}
|
|
2001
|
+
let sqrtD = sqrt(discriminant);
|
|
2002
|
+
var distance = -halfB - sqrtD;
|
|
2003
|
+
if (distance <= 0.001) {
|
|
2004
|
+
distance = -halfB + sqrtD;
|
|
2005
|
+
}
|
|
2006
|
+
if (distance <= 0.001) {
|
|
2007
|
+
return no_candidate();
|
|
2008
|
+
}
|
|
2009
|
+
let position = ray.origin.xyz + ray.direction.xyz * distance;
|
|
2010
|
+
let outward = safe_normalize((position - object.center.xyz) / radius, vec3<f32>(0.0, 1.0, 0.0));
|
|
2011
|
+
let frontFace = select(0u, 1u, dot(ray.direction.xyz, outward) < 0.0);
|
|
2012
|
+
let normal = select(-outward, outward, frontFace == 1u);
|
|
2013
|
+
return surface_candidate(
|
|
2014
|
+
distance,
|
|
2015
|
+
normal,
|
|
2016
|
+
normal,
|
|
2017
|
+
vec3<f32>(1.0, 0.0, 0.0),
|
|
2018
|
+
vec2<f32>(0.0),
|
|
2019
|
+
frontFace,
|
|
2020
|
+
0xffffffffu,
|
|
2021
|
+
object.objectId,
|
|
2022
|
+
object.objectId,
|
|
2023
|
+
0u
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
fn safe_inverse(value: f32) -> f32 {
|
|
2028
|
+
if (abs(value) < 0.000001) {
|
|
2029
|
+
return select(-1000000.0, 1000000.0, value >= 0.0);
|
|
2030
|
+
}
|
|
2031
|
+
return 1.0 / value;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
|
|
2035
|
+
let boxMin = object.center.xyz - object.halfExtent.xyz;
|
|
2036
|
+
let boxMax = object.center.xyz + object.halfExtent.xyz;
|
|
2037
|
+
let inv = vec3<f32>(
|
|
2038
|
+
safe_inverse(ray.direction.x),
|
|
2039
|
+
safe_inverse(ray.direction.y),
|
|
2040
|
+
safe_inverse(ray.direction.z)
|
|
2041
|
+
);
|
|
2042
|
+
let t0 = (boxMin - ray.origin.xyz) * inv;
|
|
2043
|
+
let t1 = (boxMax - ray.origin.xyz) * inv;
|
|
2044
|
+
let tNear = min(t0, t1);
|
|
2045
|
+
let tFar = max(t0, t1);
|
|
2046
|
+
let entry = max(max(tNear.x, tNear.y), tNear.z);
|
|
2047
|
+
let exit = min(min(tFar.x, tFar.y), tFar.z);
|
|
2048
|
+
if (exit < max(entry, 0.001)) {
|
|
2049
|
+
return no_candidate();
|
|
2050
|
+
}
|
|
2051
|
+
let distance = max(entry, 0.001);
|
|
2052
|
+
let position = ray.origin.xyz + ray.direction.xyz * distance;
|
|
2053
|
+
let rel = (position - object.center.xyz) / max(object.halfExtent.xyz, vec3<f32>(0.001));
|
|
2054
|
+
let absRel = abs(rel);
|
|
2055
|
+
var outward = vec3<f32>(0.0, 1.0, 0.0);
|
|
2056
|
+
if (absRel.x >= absRel.y && absRel.x >= absRel.z) {
|
|
2057
|
+
outward = vec3<f32>(select(-1.0, 1.0, rel.x >= 0.0), 0.0, 0.0);
|
|
2058
|
+
} else if (absRel.y >= absRel.z) {
|
|
2059
|
+
outward = vec3<f32>(0.0, select(-1.0, 1.0, rel.y >= 0.0), 0.0);
|
|
2060
|
+
} else {
|
|
2061
|
+
outward = vec3<f32>(0.0, 0.0, select(-1.0, 1.0, rel.z >= 0.0));
|
|
2062
|
+
}
|
|
2063
|
+
let frontFace = select(0u, 1u, dot(ray.direction.xyz, outward) < 0.0);
|
|
2064
|
+
let normal = select(-outward, outward, frontFace == 1u);
|
|
2065
|
+
return surface_candidate(
|
|
2066
|
+
distance,
|
|
2067
|
+
normal,
|
|
2068
|
+
normal,
|
|
2069
|
+
vec3<f32>(1.0, 0.0, 0.0),
|
|
2070
|
+
vec2<f32>(0.0),
|
|
2071
|
+
frontFace,
|
|
2072
|
+
0xffffffffu,
|
|
2073
|
+
object.objectId,
|
|
2074
|
+
object.objectId,
|
|
2075
|
+
0u
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
fn intersect_bounds(ray: RayRecord, boundsMin: vec3<f32>, boundsMax: vec3<f32>, nearest: f32) -> bool {
|
|
2080
|
+
let inv = vec3<f32>(
|
|
2081
|
+
safe_inverse(ray.direction.x),
|
|
2082
|
+
safe_inverse(ray.direction.y),
|
|
2083
|
+
safe_inverse(ray.direction.z)
|
|
2084
|
+
);
|
|
2085
|
+
let t0 = (boundsMin - ray.origin.xyz) * inv;
|
|
2086
|
+
let t1 = (boundsMax - ray.origin.xyz) * inv;
|
|
2087
|
+
let tNear = min(t0, t1);
|
|
2088
|
+
let tFar = max(t0, t1);
|
|
2089
|
+
let entry = max(max(tNear.x, tNear.y), tNear.z);
|
|
2090
|
+
let exit = min(min(tFar.x, tFar.y), tFar.z);
|
|
2091
|
+
return exit >= max(entry, 0.001) && entry <= nearest;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
fn repair_shading_normal(geometricNormal: vec3<f32>, shadingNormal: vec3<f32>) -> vec3<f32> {
|
|
2095
|
+
var normal = safe_normalize(shadingNormal, geometricNormal);
|
|
2096
|
+
if (dot(normal, geometricNormal) < 0.0) {
|
|
2097
|
+
normal = -normal;
|
|
2098
|
+
}
|
|
2099
|
+
return normal;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
fn no_candidate() -> Candidate {
|
|
2103
|
+
return Candidate(
|
|
2104
|
+
0u,
|
|
2105
|
+
0.0,
|
|
2106
|
+
vec3<f32>(0.0, 1.0, 0.0),
|
|
2107
|
+
vec3<f32>(0.0, 1.0, 0.0),
|
|
2108
|
+
vec3<f32>(0.0),
|
|
2109
|
+
vec2<f32>(0.0),
|
|
2110
|
+
1u,
|
|
2111
|
+
0xffffffffu,
|
|
2112
|
+
0xffffffffu,
|
|
2113
|
+
0u,
|
|
2114
|
+
0u
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
fn surface_candidate(
|
|
2119
|
+
distance: f32,
|
|
2120
|
+
geometricNormal: vec3<f32>,
|
|
2121
|
+
shadingNormal: vec3<f32>,
|
|
2122
|
+
barycentric: vec3<f32>,
|
|
2123
|
+
uv: vec2<f32>,
|
|
2124
|
+
frontFace: u32,
|
|
2125
|
+
triangleIndex: u32,
|
|
2126
|
+
primitiveId: u32,
|
|
2127
|
+
materialRefId: u32,
|
|
2128
|
+
mediumRefId: u32
|
|
2129
|
+
) -> Candidate {
|
|
2130
|
+
return Candidate(
|
|
2131
|
+
1u,
|
|
2132
|
+
distance,
|
|
2133
|
+
geometricNormal,
|
|
2134
|
+
shadingNormal,
|
|
2135
|
+
barycentric,
|
|
2136
|
+
uv,
|
|
2137
|
+
frontFace,
|
|
2138
|
+
triangleIndex,
|
|
2139
|
+
primitiveId,
|
|
2140
|
+
materialRefId,
|
|
2141
|
+
mediumRefId
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
fn intersect_triangle(ray: RayRecord, triangle: TriangleRecord, triangleIndex: u32) -> Candidate {
|
|
2146
|
+
let edge1 = triangle.v1.xyz - triangle.v0.xyz;
|
|
2147
|
+
let edge2 = triangle.v2.xyz - triangle.v0.xyz;
|
|
2148
|
+
let pvec = cross(ray.direction.xyz, edge2);
|
|
2149
|
+
let det = dot(edge1, pvec);
|
|
2150
|
+
if (abs(det) < 0.0000001) {
|
|
2151
|
+
return no_candidate();
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
let invDet = 1.0 / det;
|
|
2155
|
+
let tvec = ray.origin.xyz - triangle.v0.xyz;
|
|
2156
|
+
let u = dot(tvec, pvec) * invDet;
|
|
2157
|
+
if (u < 0.0 || u > 1.0) {
|
|
2158
|
+
return no_candidate();
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
let qvec = cross(tvec, edge1);
|
|
2162
|
+
let v = dot(ray.direction.xyz, qvec) * invDet;
|
|
2163
|
+
if (v < 0.0 || u + v > 1.0) {
|
|
2164
|
+
return no_candidate();
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
let distance = dot(edge2, qvec) * invDet;
|
|
2168
|
+
if (distance <= 0.001) {
|
|
2169
|
+
return no_candidate();
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
let geometric = safe_normalize(cross(edge1, edge2), vec3<f32>(0.0, 1.0, 0.0));
|
|
2173
|
+
let frontFace = select(0u, 1u, dot(ray.direction.xyz, geometric) < 0.0);
|
|
2174
|
+
let orientedGeometric = select(-geometric, geometric, frontFace == 1u);
|
|
2175
|
+
let w = 1.0 - u - v;
|
|
2176
|
+
let interpolated =
|
|
2177
|
+
triangle.n0.xyz * w +
|
|
2178
|
+
triangle.n1.xyz * u +
|
|
2179
|
+
triangle.n2.xyz * v;
|
|
2180
|
+
let shading = repair_shading_normal(orientedGeometric, interpolated);
|
|
2181
|
+
let barycentric = vec3<f32>(w, u, v);
|
|
2182
|
+
let uv =
|
|
2183
|
+
triangle.uv0uv1.xy * w +
|
|
2184
|
+
triangle.uv0uv1.zw * u +
|
|
2185
|
+
triangle.uv2Pad.xy * v;
|
|
2186
|
+
return surface_candidate(
|
|
2187
|
+
distance,
|
|
2188
|
+
orientedGeometric,
|
|
2189
|
+
shading,
|
|
2190
|
+
barycentric,
|
|
2191
|
+
uv,
|
|
2192
|
+
frontFace,
|
|
2193
|
+
triangleIndex,
|
|
2194
|
+
triangle.triangleId,
|
|
2195
|
+
triangle.materialRefId,
|
|
2196
|
+
triangle.mediumRefId
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
|
|
2201
|
+
var nearest = initialNearest;
|
|
2202
|
+
var best = no_candidate();
|
|
2203
|
+
if (config.bvhNodeCount == 0u || config.triangleCount == 0u) {
|
|
2204
|
+
return best;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
var stack = array<u32, 64>();
|
|
2208
|
+
var stackSize = 1u;
|
|
2209
|
+
stack[0] = 0u;
|
|
2210
|
+
|
|
2211
|
+
loop {
|
|
2212
|
+
if (stackSize == 0u) {
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
stackSize = stackSize - 1u;
|
|
2217
|
+
let nodeIndex = stack[stackSize];
|
|
2218
|
+
if (nodeIndex >= config.bvhNodeCount) {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
let node = bvhNodes[nodeIndex];
|
|
2223
|
+
if (!intersect_bounds(ray, node.boundsMin.xyz, node.boundsMax.xyz, nearest)) {
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
if (node.triangleCount > 0u) {
|
|
2228
|
+
for (var offset = 0u; offset < node.triangleCount; offset = offset + 1u) {
|
|
2229
|
+
let triangleIndex = node.childOrFirst + offset;
|
|
2230
|
+
if (triangleIndex >= config.triangleCount) {
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
let current = intersect_triangle(ray, triangles[triangleIndex], triangleIndex);
|
|
2234
|
+
if (current.hit == 1u && current.distance < nearest) {
|
|
2235
|
+
nearest = current.distance;
|
|
2236
|
+
best = current;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
} else {
|
|
2240
|
+
if (stackSize + 2u <= 64u) {
|
|
2241
|
+
stack[stackSize] = node.childOrFirst;
|
|
2242
|
+
stack[stackSize + 1u] = node.rightChild;
|
|
2243
|
+
stackSize = stackSize + 2u;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
return best;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
fn emission_power(emission: vec4<f32>) -> f32 {
|
|
2252
|
+
return emission.x + emission.y + emission.z;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
fn sample_weight() -> f32 {
|
|
2256
|
+
return max(config.projectionAndSampling.z, 0.000001);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
fn clamp_sample_radiance(value: vec3<f32>) -> vec3<f32> {
|
|
2260
|
+
return min(max(value, vec3<f32>(0.0)), vec3<f32>(4.0));
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
fn tone_map_radiance(value: vec3<f32>) -> vec3<f32> {
|
|
2264
|
+
let mapped = value / (vec3<f32>(1.0) + value);
|
|
2265
|
+
return pow(clamp(mapped, vec3<f32>(0.0), vec3<f32>(1.0)), vec3<f32>(1.0 / 2.2));
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
|
|
2269
|
+
return value / (vec3<f32>(1.0) + value);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
@compute @workgroup_size(64)
|
|
2273
|
+
fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
2274
|
+
let index = globalId.x;
|
|
2275
|
+
if (index == 0u) {
|
|
2276
|
+
atomicStore(&counters.activeCount, config.tilePixelCount);
|
|
2277
|
+
atomicStore(&counters.nextCount, 0u);
|
|
2278
|
+
atomicStore(&counters.terminatedCount, 0u);
|
|
2279
|
+
atomicStore(&counters.hitCount, 0u);
|
|
2280
|
+
}
|
|
2281
|
+
if (index >= config.tilePixelCount) {
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
activeQueue[index] = make_ray(index);
|
|
2285
|
+
if (u32(config.projectionAndSampling.w) == 0u) {
|
|
2286
|
+
accumulation[index] = vec4<f32>(0.0);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
@compute @workgroup_size(64)
|
|
2291
|
+
fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
2292
|
+
let index = globalId.x;
|
|
2293
|
+
let activeCount = atomicLoad(&counters.activeCount);
|
|
2294
|
+
if (index >= activeCount) {
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
let ray = activeQueue[index];
|
|
2298
|
+
var nearest = 1000000.0;
|
|
2299
|
+
var hitObject = SceneObject(
|
|
2300
|
+
0u,
|
|
2301
|
+
0u,
|
|
2302
|
+
0u,
|
|
2303
|
+
0u,
|
|
2304
|
+
vec4<f32>(0.0),
|
|
2305
|
+
vec4<f32>(0.0),
|
|
2306
|
+
vec4<f32>(0.0),
|
|
2307
|
+
vec4<f32>(0.0),
|
|
2308
|
+
vec4<f32>(1.0, 0.0, 1.0, 1.0)
|
|
2309
|
+
);
|
|
2310
|
+
var candidate = no_candidate();
|
|
2311
|
+
var hitTriangle = TriangleRecord(
|
|
2312
|
+
0u,
|
|
2313
|
+
0u,
|
|
2314
|
+
0u,
|
|
2315
|
+
0u,
|
|
2316
|
+
0u,
|
|
2317
|
+
0u,
|
|
2318
|
+
0u,
|
|
2319
|
+
0u,
|
|
2320
|
+
vec4<f32>(0.0),
|
|
2321
|
+
vec4<f32>(0.0),
|
|
2322
|
+
vec4<f32>(0.0),
|
|
2323
|
+
vec4<f32>(0.0, 1.0, 0.0, 0.0),
|
|
2324
|
+
vec4<f32>(0.0, 1.0, 0.0, 0.0),
|
|
2325
|
+
vec4<f32>(0.0, 1.0, 0.0, 0.0),
|
|
2326
|
+
vec4<f32>(0.0),
|
|
2327
|
+
vec4<f32>(0.0),
|
|
2328
|
+
vec4<f32>(0.0),
|
|
2329
|
+
vec4<f32>(0.0),
|
|
2330
|
+
vec4<f32>(1.0, 0.0, 1.0, 1.0)
|
|
2331
|
+
);
|
|
2332
|
+
|
|
2333
|
+
for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
|
|
2334
|
+
let object = sceneObjects[objectIndex];
|
|
2335
|
+
var current = no_candidate();
|
|
2336
|
+
if (object.kind == 1u) {
|
|
2337
|
+
current = intersect_sphere(ray, object);
|
|
2338
|
+
} else if (object.kind == 2u) {
|
|
2339
|
+
current = intersect_box(ray, object);
|
|
2340
|
+
}
|
|
2341
|
+
if (current.hit == 1u && current.distance < nearest) {
|
|
2342
|
+
nearest = current.distance;
|
|
2343
|
+
hitObject = object;
|
|
2344
|
+
candidate = current;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
let meshCandidate = intersect_bvh(ray, nearest);
|
|
2349
|
+
if (meshCandidate.hit == 1u && meshCandidate.distance < nearest) {
|
|
2350
|
+
nearest = meshCandidate.distance;
|
|
2351
|
+
candidate = meshCandidate;
|
|
2352
|
+
hitTriangle = triangles[meshCandidate.triangleIndex];
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
if (candidate.hit == 0u) {
|
|
2356
|
+
hits[index] = make_miss(ray);
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
let position = ray.origin.xyz + ray.direction.xyz * candidate.distance;
|
|
2361
|
+
let hitMaterialKind = select(hitObject.materialKind, hitTriangle.materialKind, candidate.triangleIndex != 0xffffffffu);
|
|
2362
|
+
let hitObjectId = select(hitObject.objectId, hitTriangle.meshId, candidate.triangleIndex != 0xffffffffu);
|
|
2363
|
+
let hitColor = select(hitObject.color, hitTriangle.color, candidate.triangleIndex != 0xffffffffu);
|
|
2364
|
+
let hitEmission = select(hitObject.emission, hitTriangle.emission, candidate.triangleIndex != 0xffffffffu);
|
|
2365
|
+
let hitMaterial = select(hitObject.material, hitTriangle.material, candidate.triangleIndex != 0xffffffffu);
|
|
2366
|
+
let hitPrimitiveId = select(candidate.primitiveId, hitTriangle.triangleId, candidate.triangleIndex != 0xffffffffu);
|
|
2367
|
+
let hitMaterialRefId = select(candidate.materialRefId, hitTriangle.materialRefId, candidate.triangleIndex != 0xffffffffu);
|
|
2368
|
+
let hitMediumRefId = select(candidate.mediumRefId, hitTriangle.mediumRefId, candidate.triangleIndex != 0xffffffffu);
|
|
2369
|
+
var hitType = 0u;
|
|
2370
|
+
if (hitMaterialKind == 4u || emission_power(hitEmission) > 0.0001) {
|
|
2371
|
+
hitType = 1u;
|
|
2372
|
+
} else if (hitMaterialKind == 3u || hitMaterial.z < 0.999) {
|
|
2373
|
+
hitType = 3u;
|
|
2374
|
+
}
|
|
2375
|
+
atomicAdd(&counters.hitCount, 1u);
|
|
2376
|
+
hits[index] = HitRecord(
|
|
2377
|
+
ray.rayId,
|
|
2378
|
+
ray.sourcePixelId,
|
|
2379
|
+
hitType,
|
|
2380
|
+
hitObjectId,
|
|
2381
|
+
hitMaterialKind,
|
|
2382
|
+
candidate.frontFace,
|
|
2383
|
+
hitPrimitiveId,
|
|
2384
|
+
hitMaterialRefId,
|
|
2385
|
+
hitMediumRefId,
|
|
2386
|
+
0u,
|
|
2387
|
+
0u,
|
|
2388
|
+
0u,
|
|
2389
|
+
candidate.distance,
|
|
2390
|
+
vec3<f32>(0.0),
|
|
2391
|
+
vec4<f32>(position, 1.0),
|
|
2392
|
+
vec4<f32>(candidate.geometricNormal, 0.0),
|
|
2393
|
+
vec4<f32>(candidate.shadingNormal, 0.0),
|
|
2394
|
+
vec4<f32>(candidate.barycentric, 0.0),
|
|
2395
|
+
vec4<f32>(candidate.uv, 0.0, 0.0),
|
|
2396
|
+
hitColor,
|
|
2397
|
+
hitEmission,
|
|
2398
|
+
hitMaterial
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
fn offset_origin(position: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
|
|
2403
|
+
return position + normal * 0.0025;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
fn random_unit_vector(seed: u32) -> vec3<f32> {
|
|
2407
|
+
let z = random01(seed) * 2.0 - 1.0;
|
|
2408
|
+
let a = random01(seed + 11u) * 6.28318530718;
|
|
2409
|
+
let r = sqrt(max(0.0, 1.0 - z * z));
|
|
2410
|
+
return vec3<f32>(r * cos(a), r * sin(a), z);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
fn schlick(cosine: f32, refractionRatio: f32) -> f32 {
|
|
2414
|
+
var r0 = (1.0 - refractionRatio) / (1.0 + refractionRatio);
|
|
2415
|
+
r0 = r0 * r0;
|
|
2416
|
+
return r0 + (1.0 - r0) * pow(1.0 - cosine, 5.0);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
fn refract_direction(unitDirection: vec3<f32>, normal: vec3<f32>, etaRatio: f32) -> vec3<f32> {
|
|
2420
|
+
let cosTheta = min(dot(-unitDirection, normal), 1.0);
|
|
2421
|
+
let rOutPerp = etaRatio * (unitDirection + cosTheta * normal);
|
|
2422
|
+
let rOutParallel = -sqrt(abs(1.0 - dot(rOutPerp, rOutPerp))) * normal;
|
|
2423
|
+
return safe_normalize(rOutPerp + rOutParallel, reflect(unitDirection, normal));
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
fn sample_emissive_triangle_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
|
|
2427
|
+
if (config.emissiveTriangleCount == 0u) {
|
|
2428
|
+
return fallback;
|
|
2429
|
+
}
|
|
2430
|
+
let lightSlot = min(u32(random01(seed + 71u) * f32(config.emissiveTriangleCount)), config.emissiveTriangleCount - 1u);
|
|
2431
|
+
let lightMetadata = bvhNodes[config.bvhNodeCapacity + lightSlot];
|
|
2432
|
+
let triangleIndex = lightMetadata.childOrFirst;
|
|
2433
|
+
if (triangleIndex >= config.triangleCount) {
|
|
2434
|
+
return fallback;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
let lightTriangle = triangles[triangleIndex];
|
|
2438
|
+
let r1 = random01(seed + 101u);
|
|
2439
|
+
let r2 = random01(seed + 193u);
|
|
2440
|
+
let root = sqrt(r1);
|
|
2441
|
+
let b0 = 1.0 - root;
|
|
2442
|
+
let b1 = root * (1.0 - r2);
|
|
2443
|
+
let b2 = root * r2;
|
|
2444
|
+
let lightPoint =
|
|
2445
|
+
lightTriangle.v0.xyz * b0 +
|
|
2446
|
+
lightTriangle.v1.xyz * b1 +
|
|
2447
|
+
lightTriangle.v2.xyz * b2;
|
|
2448
|
+
return safe_normalize(lightPoint - hit.position.xyz, fallback);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
|
|
2452
|
+
if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
|
|
2453
|
+
return fallback;
|
|
2454
|
+
}
|
|
2455
|
+
let portalSlot = min(
|
|
2456
|
+
u32(random01(seed + 211u) * f32(config.environmentPortalCount)),
|
|
2457
|
+
config.environmentPortalCount - 1u
|
|
2458
|
+
);
|
|
2459
|
+
let portal = environmentPortals[portalSlot];
|
|
2460
|
+
let u = (random01(seed + 223u) * 2.0 - 1.0) * portal.tangent.w;
|
|
2461
|
+
let v = (random01(seed + 227u) * 2.0 - 1.0) * portal.bitangent.w;
|
|
2462
|
+
let portalTarget = portal.position.xyz + portal.tangent.xyz * u + portal.bitangent.xyz * v;
|
|
2463
|
+
return safe_normalize(portalTarget - hit.position.xyz, fallback);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
|
|
2467
|
+
let roughness = clamp(hit.material.x, 0.0, 1.0);
|
|
2468
|
+
if (hit.materialKind == 1u) {
|
|
2469
|
+
return ScatterResult(
|
|
2470
|
+
vec4<f32>(
|
|
2471
|
+
safe_normalize(
|
|
2472
|
+
reflect(ray.direction.xyz, hit.shadingNormal.xyz) + random_unit_vector(seed) * roughness,
|
|
2473
|
+
hit.shadingNormal.xyz
|
|
2474
|
+
),
|
|
2475
|
+
0.0
|
|
2476
|
+
),
|
|
2477
|
+
0u,
|
|
2478
|
+
0u,
|
|
2479
|
+
0u,
|
|
2480
|
+
0u
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
if (hit.materialKind == 2u || hit.materialKind == 3u) {
|
|
2485
|
+
let ior = max(hit.material.w, 1.01);
|
|
2486
|
+
let etaRatio = select(ior, 1.0 / ior, hit.frontFace == 1u);
|
|
2487
|
+
let cosTheta = min(dot(-ray.direction.xyz, hit.shadingNormal.xyz), 1.0);
|
|
2488
|
+
let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
|
|
2489
|
+
let cannotRefract = etaRatio * sinTheta > 1.0;
|
|
2490
|
+
let reflectChance = schlick(cosTheta, etaRatio);
|
|
2491
|
+
if (cannotRefract || random01(seed + 23u) < reflectChance) {
|
|
2492
|
+
return ScatterResult(vec4<f32>(reflect(ray.direction.xyz, hit.shadingNormal.xyz), 0.0), 0u, 0u, 0u, 0u);
|
|
2493
|
+
}
|
|
2494
|
+
return ScatterResult(vec4<f32>(refract_direction(ray.direction.xyz, hit.shadingNormal.xyz, etaRatio), 0.0), 0u, 0u, 0u, 0u);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
let randomDiffuse = safe_normalize(
|
|
2498
|
+
hit.shadingNormal.xyz + random_unit_vector(seed),
|
|
2499
|
+
hit.shadingNormal.xyz
|
|
2500
|
+
);
|
|
2501
|
+
let guidedLight = sample_emissive_triangle_direction(hit, seed, randomDiffuse);
|
|
2502
|
+
let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
|
|
2503
|
+
let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
|
|
2504
|
+
let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
|
|
2505
|
+
let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
|
|
2506
|
+
let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
|
|
2507
|
+
let useGuidedPortal =
|
|
2508
|
+
!useGuidedLight &&
|
|
2509
|
+
canSamplePortal &&
|
|
2510
|
+
config.environmentPortalCount > 0u &&
|
|
2511
|
+
config.environmentPortalMode > 0u &&
|
|
2512
|
+
random01(seed + 89u) < 0.58;
|
|
2513
|
+
let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
|
|
2514
|
+
return ScatterResult(
|
|
2515
|
+
vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
|
|
2516
|
+
select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
|
|
2517
|
+
0u,
|
|
2518
|
+
0u,
|
|
2519
|
+
0u
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
@compute @workgroup_size(64)
|
|
2524
|
+
fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
2525
|
+
let index = globalId.x;
|
|
2526
|
+
let activeCount = atomicLoad(&counters.activeCount);
|
|
2527
|
+
if (index >= activeCount) {
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
let ray = activeQueue[index];
|
|
2532
|
+
let hit = hits[index];
|
|
2533
|
+
var contribution = vec3<f32>(0.0);
|
|
2534
|
+
|
|
2535
|
+
if (hit.hitType == 1u) {
|
|
2536
|
+
let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
|
|
2537
|
+
contribution = clamp_sample_radiance(
|
|
2538
|
+
ray.throughput.xyz * max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight
|
|
2539
|
+
);
|
|
2540
|
+
accumulation[ray.rayId] =
|
|
2541
|
+
accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
|
|
2542
|
+
atomicAdd(&counters.terminatedCount, 1u);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
if (hit.hitType == 2u) {
|
|
2547
|
+
contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
|
|
2548
|
+
accumulation[ray.rayId] =
|
|
2549
|
+
accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
|
|
2550
|
+
atomicAdd(&counters.terminatedCount, 1u);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
if (ray.bounce + 1u >= config.maxDepth) {
|
|
2555
|
+
accumulation[ray.rayId] =
|
|
2556
|
+
accumulation[ray.rayId] +
|
|
2557
|
+
vec4<f32>(ray.throughput.xyz * config.ambientColor.xyz * sample_weight(), 1.0);
|
|
2558
|
+
atomicAdd(&counters.terminatedCount, 1u);
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
let seed = mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 11u);
|
|
2563
|
+
let scatter = scatter_direction(ray, hit, seed);
|
|
2564
|
+
let nextIndex = atomicAdd(&counters.nextCount, 1u);
|
|
2565
|
+
if (nextIndex >= config.tilePixelCount) {
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
2569
|
+
let opacity = clamp(hit.material.z, 0.0, 1.0);
|
|
2570
|
+
let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
|
|
2571
|
+
let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
|
|
2572
|
+
let throughput = ray.throughput.xyz * mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
|
|
2573
|
+
nextQueue[nextIndex] = RayRecord(
|
|
2574
|
+
ray.rayId,
|
|
2575
|
+
ray.rayId,
|
|
2576
|
+
ray.sourcePixelId,
|
|
2577
|
+
ray.sampleId,
|
|
2578
|
+
ray.bounce + 1u,
|
|
2579
|
+
ray.mediumRefId,
|
|
2580
|
+
scatter.flags,
|
|
2581
|
+
0u,
|
|
2582
|
+
vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
|
|
2583
|
+
scatter.direction,
|
|
2584
|
+
vec4<f32>(throughput, ray.throughput.w)
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
@compute @workgroup_size(1)
|
|
2589
|
+
fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
2590
|
+
if (globalId.x > 0u) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
let nextCount = atomicLoad(&counters.nextCount);
|
|
2594
|
+
atomicStore(&counters.activeCount, min(nextCount, config.tilePixelCount));
|
|
2595
|
+
atomicStore(&counters.nextCount, 0u);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
@compute @workgroup_size(64)
|
|
2599
|
+
fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
2600
|
+
let index = globalId.x;
|
|
2601
|
+
if (index >= config.tilePixelCount) {
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
let localX = index % config.tileWidth;
|
|
2605
|
+
let localY = index / config.tileWidth;
|
|
2606
|
+
let pixel = vec2<i32>(i32(config.tileX + localX), i32(config.tileY + localY));
|
|
2607
|
+
let radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
|
|
2608
|
+
|
|
2609
|
+
textureStore(radianceImage, pixel, vec4<f32>(radiance, 1.0));
|
|
2610
|
+
if (config.denoise == 0u) {
|
|
2611
|
+
textureStore(outputImage, pixel, vec4<f32>(tone_map_radiance(radiance), 1.0));
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
@compute @workgroup_size(8, 8)
|
|
2616
|
+
fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
2617
|
+
let x = globalId.x;
|
|
2618
|
+
let y = globalId.y;
|
|
2619
|
+
if (x >= config.canvasWidth || y >= config.canvasHeight) {
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
let pixel = vec2<i32>(i32(x), i32(y));
|
|
2624
|
+
let center = textureLoad(denoiseInputRadiance, pixel, 0).xyz;
|
|
2625
|
+
var sum = center * 1.4;
|
|
2626
|
+
var totalWeight = 1.4;
|
|
2627
|
+
let centerRange = denoise_range_space(center);
|
|
2628
|
+
|
|
2629
|
+
for (var oy = -2i; oy <= 2i; oy = oy + 1i) {
|
|
2630
|
+
for (var ox = -2i; ox <= 2i; ox = ox + 1i) {
|
|
2631
|
+
if (ox == 0i && oy == 0i) {
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
let sx = clamp(i32(x) + ox, 0i, i32(config.canvasWidth) - 1i);
|
|
2635
|
+
let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
|
|
2636
|
+
let sampleColor = textureLoad(denoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
|
|
2637
|
+
let colorDistance = length(denoise_range_space(sampleColor) - centerRange);
|
|
2638
|
+
let rangeWeight = 1.0 / (1.0 + colorDistance * 7.0);
|
|
2639
|
+
let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * 0.24);
|
|
2640
|
+
let diagonalWeight = select(1.0, 0.78, abs(ox) + abs(oy) > 2i);
|
|
2641
|
+
let weight = rangeWeight * diagonalWeight * distanceWeight;
|
|
2642
|
+
sum = sum + sampleColor * weight;
|
|
2643
|
+
totalWeight = totalWeight + weight;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
let filtered = sum / max(totalWeight, 0.0001);
|
|
2648
|
+
let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.4);
|
|
2649
|
+
let color = min(mix(center, filtered, 0.52 + outlier * 0.18), vec3<f32>(16.0));
|
|
2650
|
+
textureStore(denoisedRadianceImage, pixel, vec4<f32>(color, 1.0));
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
@compute @workgroup_size(8, 8)
|
|
2654
|
+
fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
2655
|
+
let x = globalId.x;
|
|
2656
|
+
let y = globalId.y;
|
|
2657
|
+
if (x >= config.canvasWidth || y >= config.canvasHeight) {
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
let pixel = vec2<i32>(i32(x), i32(y));
|
|
2662
|
+
let center = textureLoad(finalDenoiseInputRadiance, pixel, 0).xyz;
|
|
2663
|
+
var sum = center * 1.25;
|
|
2664
|
+
var totalWeight = 1.25;
|
|
2665
|
+
let centerRange = denoise_range_space(center);
|
|
2666
|
+
|
|
2667
|
+
for (var oy = -1i; oy <= 1i; oy = oy + 1i) {
|
|
2668
|
+
for (var ox = -1i; ox <= 1i; ox = ox + 1i) {
|
|
2669
|
+
if (ox == 0i && oy == 0i) {
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
let sx = clamp(i32(x) + ox, 0i, i32(config.canvasWidth) - 1i);
|
|
2673
|
+
let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
|
|
2674
|
+
let sampleColor = textureLoad(finalDenoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
|
|
2675
|
+
let colorDistance = length(denoise_range_space(sampleColor) - centerRange);
|
|
2676
|
+
let rangeWeight = 1.0 / (1.0 + colorDistance * 9.0);
|
|
2677
|
+
let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * 0.4);
|
|
2678
|
+
let weight = rangeWeight * distanceWeight;
|
|
2679
|
+
sum = sum + sampleColor * weight;
|
|
2680
|
+
totalWeight = totalWeight + weight;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
let filtered = sum / max(totalWeight, 0.0001);
|
|
2685
|
+
let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.8);
|
|
2686
|
+
let radiance = min(mix(center, filtered, 0.28 + outlier * 0.12), vec3<f32>(16.0));
|
|
2687
|
+
textureStore(denoisedOutputImage, pixel, vec4<f32>(tone_map_radiance(radiance), 1.0));
|
|
2688
|
+
}
|
|
2689
|
+
`;
|
|
2690
|
+
var PRESENT_WGSL = `
|
|
2691
|
+
struct VertexOut {
|
|
2692
|
+
@builtin(position) position: vec4<f32>,
|
|
2693
|
+
@location(0) uv: vec2<f32>,
|
|
2694
|
+
};
|
|
2695
|
+
|
|
2696
|
+
@vertex
|
|
2697
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOut {
|
|
2698
|
+
var positions = array<vec2<f32>, 3>(
|
|
2699
|
+
vec2<f32>(-1.0, -1.0),
|
|
2700
|
+
vec2<f32>(3.0, -1.0),
|
|
2701
|
+
vec2<f32>(-1.0, 3.0)
|
|
2702
|
+
);
|
|
2703
|
+
var uvs = array<vec2<f32>, 3>(
|
|
2704
|
+
vec2<f32>(0.0, 1.0),
|
|
2705
|
+
vec2<f32>(2.0, 1.0),
|
|
2706
|
+
vec2<f32>(0.0, -1.0)
|
|
2707
|
+
);
|
|
2708
|
+
var out: VertexOut;
|
|
2709
|
+
out.position = vec4<f32>(positions[vertexIndex], 0.0, 1.0);
|
|
2710
|
+
out.uv = uvs[vertexIndex];
|
|
2711
|
+
return out;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
@group(0) @binding(0) var renderTexture: texture_2d<f32>;
|
|
2715
|
+
@group(0) @binding(1) var renderSampler: sampler;
|
|
2716
|
+
|
|
2717
|
+
@fragment
|
|
2718
|
+
fn fragmentMain(in: VertexOut) -> @location(0) vec4<f32> {
|
|
2719
|
+
return textureSample(renderTexture, renderSampler, in.uv);
|
|
2720
|
+
}
|
|
2721
|
+
`;
|
|
2722
|
+
function createWavefrontDeviceDescriptor(adapter, options = {}) {
|
|
2723
|
+
const requiredLimits = { ...options.requiredLimits ?? {} };
|
|
2724
|
+
const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
|
|
2725
|
+
if (Number.isFinite(exposedStorageBufferLimit)) {
|
|
2726
|
+
if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
|
|
2727
|
+
throw new Error(
|
|
2728
|
+
`Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, but this adapter exposes ${exposedStorageBufferLimit}.`
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
|
|
2732
|
+
Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
|
|
2733
|
+
TRACE_STORAGE_BUFFER_BINDINGS
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
const descriptor = { ...options.deviceDescriptor ?? {} };
|
|
2737
|
+
if (Object.keys(requiredLimits).length > 0) {
|
|
2738
|
+
descriptor.requiredLimits = {
|
|
2739
|
+
...descriptor.requiredLimits ?? {},
|
|
2740
|
+
...requiredLimits
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
return Object.keys(descriptor).length > 0 ? descriptor : void 0;
|
|
2744
|
+
}
|
|
2745
|
+
async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
2746
|
+
assertAnalyticDisplayQualityPolicy(options);
|
|
2747
|
+
const constants = getGpuUsageConstants();
|
|
2748
|
+
const navigatorRef = options.navigator ?? globalThis.navigator;
|
|
2749
|
+
if (!supportsWavefrontPathTracingCompute({ navigator: navigatorRef })) {
|
|
2750
|
+
throw new Error("WebGPU wavefront path tracing requires navigator.gpu.");
|
|
2751
|
+
}
|
|
2752
|
+
const canvas = resolveCanvas(options.canvas, options.document);
|
|
2753
|
+
const initialConfig = createWavefrontPathTracingComputeConfig({
|
|
2754
|
+
...options,
|
|
2755
|
+
canvas
|
|
2756
|
+
});
|
|
2757
|
+
const adapter = await navigatorRef.gpu.requestAdapter({
|
|
2758
|
+
powerPreference: options.powerPreference ?? "high-performance"
|
|
2759
|
+
});
|
|
2760
|
+
if (!adapter) {
|
|
2761
|
+
throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
|
|
2762
|
+
}
|
|
2763
|
+
const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
|
|
2764
|
+
const context = canvas.getContext("webgpu");
|
|
2765
|
+
if (!context || typeof context.configure !== "function") {
|
|
2766
|
+
throw new Error("Canvas WebGPU context does not support configure().");
|
|
2767
|
+
}
|
|
2768
|
+
const format = options.format ?? (typeof navigatorRef.gpu.getPreferredCanvasFormat === "function" ? navigatorRef.gpu.getPreferredCanvasFormat() : "bgra8unorm");
|
|
2769
|
+
let config = initialConfig;
|
|
2770
|
+
const safeTileSize = clampTileSizeForDevice(config, device);
|
|
2771
|
+
if (safeTileSize !== config.tileSize) {
|
|
2772
|
+
config = createWavefrontPathTracingComputeConfig({
|
|
2773
|
+
...options,
|
|
2774
|
+
canvas,
|
|
2775
|
+
width: config.width,
|
|
2776
|
+
height: config.height,
|
|
2777
|
+
tileSize: safeTileSize
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
canvas.width = config.width;
|
|
2781
|
+
canvas.height = config.height;
|
|
2782
|
+
context.configure({
|
|
2783
|
+
device,
|
|
2784
|
+
format,
|
|
2785
|
+
alphaMode: options.alpha === true ? "premultiplied" : "opaque"
|
|
2786
|
+
});
|
|
2787
|
+
const bufferUsage = constants.buffer.STORAGE | constants.buffer.COPY_DST;
|
|
2788
|
+
const rayQueueBytes = config.tilePixelCapacity * RAY_RECORD_BYTES;
|
|
2789
|
+
const hitBytes = config.tilePixelCapacity * HIT_RECORD_BYTES;
|
|
2790
|
+
const accumulationBytes = config.tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
|
|
2791
|
+
const activeQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.activeQueue");
|
|
2792
|
+
const nextQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.nextQueue");
|
|
2793
|
+
const hitBuffer = createBuffer(device, bufferUsage, hitBytes, "plasius.wavefront.hitBuffer");
|
|
2794
|
+
const accumulationBuffer = createBuffer(
|
|
2795
|
+
device,
|
|
2796
|
+
bufferUsage,
|
|
2797
|
+
accumulationBytes,
|
|
2798
|
+
"plasius.wavefront.accumulation"
|
|
2799
|
+
);
|
|
2800
|
+
const sceneObjectBuffer = createBuffer(
|
|
2801
|
+
device,
|
|
2802
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2803
|
+
config.sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES,
|
|
2804
|
+
"plasius.wavefront.sceneObjects"
|
|
2805
|
+
);
|
|
2806
|
+
const triangleBuffer = createBuffer(
|
|
2807
|
+
device,
|
|
2808
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2809
|
+
Math.max(1, config.triangleCapacity) * TRIANGLE_RECORD_BYTES,
|
|
2810
|
+
"plasius.wavefront.triangles"
|
|
2811
|
+
);
|
|
2812
|
+
const bvhNodeBuffer = createBuffer(
|
|
2813
|
+
device,
|
|
2814
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2815
|
+
Math.max(1, config.bvhNodeCapacity + config.emissiveTriangleCapacity) * BVH_NODE_RECORD_BYTES,
|
|
2816
|
+
"plasius.wavefront.bvhNodes"
|
|
2817
|
+
);
|
|
2818
|
+
const meshVertexBuffer = createBuffer(
|
|
2819
|
+
device,
|
|
2820
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2821
|
+
Math.max(1, config.gpuMeshSource.vertices.count) * MESH_VERTEX_RECORD_BYTES,
|
|
2822
|
+
"plasius.wavefront.meshVertices"
|
|
2823
|
+
);
|
|
2824
|
+
const meshIndexBuffer = createBuffer(
|
|
2825
|
+
device,
|
|
2826
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2827
|
+
Math.max(1, config.gpuMeshSource.indices.count) * 4,
|
|
2828
|
+
"plasius.wavefront.meshIndices"
|
|
2829
|
+
);
|
|
2830
|
+
const meshRangeBuffer = createBuffer(
|
|
2831
|
+
device,
|
|
2832
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2833
|
+
Math.max(1, config.gpuMeshSource.meshes.count) * MESH_RANGE_RECORD_BYTES,
|
|
2834
|
+
"plasius.wavefront.meshRanges"
|
|
2835
|
+
);
|
|
2836
|
+
const environmentPortalBuffer = createBuffer(
|
|
2837
|
+
device,
|
|
2838
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2839
|
+
Math.max(1, config.environmentPortalCapacity) * ENVIRONMENT_PORTAL_RECORD_BYTES,
|
|
2840
|
+
"plasius.wavefront.environmentPortals"
|
|
2841
|
+
);
|
|
2842
|
+
const bvhLeafRefBuffer = createBuffer(
|
|
2843
|
+
device,
|
|
2844
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2845
|
+
Math.max(1, config.bvhLeafSortCapacity) * BVH_LEAF_REF_RECORD_BYTES,
|
|
2846
|
+
"plasius.wavefront.bvhLeafRefs"
|
|
2847
|
+
);
|
|
2848
|
+
const tiles = createTiles(config.width, config.height, config.tileSize);
|
|
2849
|
+
const uniformOffsetAlignment = Number(device?.limits?.minUniformBufferOffsetAlignment);
|
|
2850
|
+
const configBufferStride = alignTo(
|
|
2851
|
+
CONFIG_BUFFER_BYTES,
|
|
2852
|
+
Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
|
|
2853
|
+
);
|
|
2854
|
+
const frameConfigSlotCount = Math.max(
|
|
2855
|
+
1,
|
|
2856
|
+
tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
|
|
2857
|
+
);
|
|
2858
|
+
const configBuffer = createBuffer(
|
|
2859
|
+
device,
|
|
2860
|
+
constants.buffer.UNIFORM | constants.buffer.COPY_DST,
|
|
2861
|
+
frameConfigSlotCount * configBufferStride,
|
|
2862
|
+
"plasius.wavefront.frameConfig"
|
|
2863
|
+
);
|
|
2864
|
+
const bvhBuildConfigSlots = 1 + config.bvhSortStages.length + config.bvhBuildLevels.length;
|
|
2865
|
+
const bvhBuildConfigBuffer = createBuffer(
|
|
2866
|
+
device,
|
|
2867
|
+
constants.buffer.UNIFORM | constants.buffer.COPY_DST,
|
|
2868
|
+
Math.max(1, bvhBuildConfigSlots) * configBufferStride,
|
|
2869
|
+
"plasius.wavefront.bvhBuildConfig"
|
|
2870
|
+
);
|
|
2871
|
+
const counterBuffer = createBuffer(
|
|
2872
|
+
device,
|
|
2873
|
+
constants.buffer.STORAGE | constants.buffer.COPY_DST,
|
|
2874
|
+
COUNTER_BUFFER_BYTES,
|
|
2875
|
+
"plasius.wavefront.counters"
|
|
2876
|
+
);
|
|
2877
|
+
let packedScene = packWavefrontSceneObjects(config.sceneObjects, config.sceneObjectCapacity);
|
|
2878
|
+
device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
|
|
2879
|
+
const packedTriangles = packWavefrontTriangles(
|
|
2880
|
+
config.meshAcceleration.triangles,
|
|
2881
|
+
Math.max(1, config.triangleCapacity)
|
|
2882
|
+
);
|
|
2883
|
+
const packedBvhNodes = packWavefrontBvhNodes(
|
|
2884
|
+
config.meshAcceleration.nodes,
|
|
2885
|
+
Math.max(1, config.bvhNodeCapacity + config.emissiveTriangleCapacity)
|
|
2886
|
+
);
|
|
2887
|
+
const packedBvhNodeUints = new Uint32Array(packedBvhNodes.buffer);
|
|
2888
|
+
config.emissiveTriangleIndices.indices.forEach((triangleIndex, index) => {
|
|
2889
|
+
const nodeOffset = (config.bvhNodeCapacity + index) * (BVH_NODE_RECORD_BYTES / 4);
|
|
2890
|
+
packedBvhNodeUints[nodeOffset + 8] = triangleIndex;
|
|
2891
|
+
});
|
|
2892
|
+
device.queue.writeBuffer(triangleBuffer, 0, packedTriangles.buffer);
|
|
2893
|
+
device.queue.writeBuffer(bvhNodeBuffer, 0, packedBvhNodes.buffer);
|
|
2894
|
+
device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
|
|
2895
|
+
device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
|
|
2896
|
+
device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
|
|
2897
|
+
const packedEnvironmentPortals = packEnvironmentPortals(
|
|
2898
|
+
config.environmentPortals,
|
|
2899
|
+
Math.max(1, config.environmentPortalCapacity)
|
|
2900
|
+
);
|
|
2901
|
+
device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
|
|
2902
|
+
const radianceTexture = device.createTexture({
|
|
2903
|
+
label: "plasius.wavefront.radiance",
|
|
2904
|
+
size: { width: config.width, height: config.height },
|
|
2905
|
+
format: "rgba16float",
|
|
2906
|
+
usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING
|
|
2907
|
+
});
|
|
2908
|
+
const radianceView = radianceTexture.createView();
|
|
2909
|
+
const denoiseScratchTexture = device.createTexture({
|
|
2910
|
+
label: "plasius.wavefront.denoiseScratch",
|
|
2911
|
+
size: { width: config.width, height: config.height },
|
|
2912
|
+
format: "rgba16float",
|
|
2913
|
+
usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING
|
|
2914
|
+
});
|
|
2915
|
+
const denoiseScratchView = denoiseScratchTexture.createView();
|
|
2916
|
+
const outputTexture = device.createTexture({
|
|
2917
|
+
label: "plasius.wavefront.output",
|
|
2918
|
+
size: { width: config.width, height: config.height },
|
|
2919
|
+
format: "rgba8unorm",
|
|
2920
|
+
usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_SRC
|
|
2921
|
+
});
|
|
2922
|
+
const outputView = outputTexture.createView();
|
|
2923
|
+
const sampler = device.createSampler({
|
|
2924
|
+
label: "plasius.wavefront.presentSampler",
|
|
2925
|
+
magFilter: "nearest",
|
|
2926
|
+
minFilter: "nearest"
|
|
2927
|
+
});
|
|
2928
|
+
const traceBindGroupLayout = device.createBindGroupLayout({
|
|
2929
|
+
label: "plasius.wavefront.traceBindGroupLayout",
|
|
2930
|
+
entries: [
|
|
2931
|
+
{ binding: 0, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2932
|
+
{ binding: 1, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2933
|
+
{ binding: 2, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2934
|
+
{ binding: 3, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2935
|
+
{ binding: 4, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
2936
|
+
{
|
|
2937
|
+
binding: 5,
|
|
2938
|
+
visibility: constants.shader.COMPUTE,
|
|
2939
|
+
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: CONFIG_BUFFER_BYTES }
|
|
2940
|
+
},
|
|
2941
|
+
{ binding: 6, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2942
|
+
{
|
|
2943
|
+
binding: 7,
|
|
2944
|
+
visibility: constants.shader.COMPUTE,
|
|
2945
|
+
storageTexture: { access: "write-only", format: "rgba8unorm" }
|
|
2946
|
+
},
|
|
2947
|
+
{ binding: 8, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2948
|
+
{ binding: 9, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2949
|
+
{
|
|
2950
|
+
binding: 16,
|
|
2951
|
+
visibility: constants.shader.COMPUTE,
|
|
2952
|
+
storageTexture: { access: "write-only", format: "rgba16float" }
|
|
2953
|
+
},
|
|
2954
|
+
{ binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } }
|
|
2955
|
+
]
|
|
2956
|
+
});
|
|
2957
|
+
const accelerationBindGroupLayout = device.createBindGroupLayout({
|
|
2958
|
+
label: "plasius.wavefront.accelerationBindGroupLayout",
|
|
2959
|
+
entries: [
|
|
2960
|
+
{
|
|
2961
|
+
binding: 5,
|
|
2962
|
+
visibility: constants.shader.COMPUTE,
|
|
2963
|
+
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: CONFIG_BUFFER_BYTES }
|
|
2964
|
+
},
|
|
2965
|
+
{ binding: 8, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2966
|
+
{ binding: 9, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
|
|
2967
|
+
{ binding: 10, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
2968
|
+
{ binding: 11, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
2969
|
+
{ binding: 12, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
2970
|
+
{ binding: 13, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } }
|
|
2971
|
+
]
|
|
2972
|
+
});
|
|
2973
|
+
const denoiseRadianceBindGroupLayout = device.createBindGroupLayout({
|
|
2974
|
+
label: "plasius.wavefront.denoiseRadianceBindGroupLayout",
|
|
2975
|
+
entries: [
|
|
2976
|
+
{
|
|
2977
|
+
binding: 5,
|
|
2978
|
+
visibility: constants.shader.COMPUTE,
|
|
2979
|
+
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: CONFIG_BUFFER_BYTES }
|
|
2980
|
+
},
|
|
2981
|
+
{
|
|
2982
|
+
binding: 14,
|
|
2983
|
+
visibility: constants.shader.COMPUTE,
|
|
2984
|
+
texture: { sampleType: "float" }
|
|
2985
|
+
},
|
|
2986
|
+
{
|
|
2987
|
+
binding: 15,
|
|
2988
|
+
visibility: constants.shader.COMPUTE,
|
|
2989
|
+
storageTexture: { access: "write-only", format: "rgba16float" }
|
|
2990
|
+
}
|
|
2991
|
+
]
|
|
2992
|
+
});
|
|
2993
|
+
const denoiseResolveBindGroupLayout = device.createBindGroupLayout({
|
|
2994
|
+
label: "plasius.wavefront.denoiseResolveBindGroupLayout",
|
|
2995
|
+
entries: [
|
|
2996
|
+
{
|
|
2997
|
+
binding: 5,
|
|
2998
|
+
visibility: constants.shader.COMPUTE,
|
|
2999
|
+
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: CONFIG_BUFFER_BYTES }
|
|
3000
|
+
},
|
|
3001
|
+
{
|
|
3002
|
+
binding: 17,
|
|
3003
|
+
visibility: constants.shader.COMPUTE,
|
|
3004
|
+
texture: { sampleType: "float" }
|
|
3005
|
+
},
|
|
3006
|
+
{
|
|
3007
|
+
binding: 18,
|
|
3008
|
+
visibility: constants.shader.COMPUTE,
|
|
3009
|
+
storageTexture: { access: "write-only", format: "rgba8unorm" }
|
|
3010
|
+
}
|
|
3011
|
+
]
|
|
3012
|
+
});
|
|
3013
|
+
const tracePipelineLayout = device.createPipelineLayout({
|
|
3014
|
+
label: "plasius.wavefront.tracePipelineLayout",
|
|
3015
|
+
bindGroupLayouts: [traceBindGroupLayout]
|
|
3016
|
+
});
|
|
3017
|
+
const accelerationPipelineLayout = device.createPipelineLayout({
|
|
3018
|
+
label: "plasius.wavefront.accelerationPipelineLayout",
|
|
3019
|
+
bindGroupLayouts: [accelerationBindGroupLayout]
|
|
3020
|
+
});
|
|
3021
|
+
const denoiseRadiancePipelineLayout = device.createPipelineLayout({
|
|
3022
|
+
label: "plasius.wavefront.denoiseRadiancePipelineLayout",
|
|
3023
|
+
bindGroupLayouts: [denoiseRadianceBindGroupLayout]
|
|
3024
|
+
});
|
|
3025
|
+
const denoiseResolvePipelineLayout = device.createPipelineLayout({
|
|
3026
|
+
label: "plasius.wavefront.denoiseResolvePipelineLayout",
|
|
3027
|
+
bindGroupLayouts: [denoiseResolveBindGroupLayout]
|
|
3028
|
+
});
|
|
3029
|
+
const computeShader = device.createShaderModule({
|
|
3030
|
+
label: "plasius.wavefront.computeShader",
|
|
3031
|
+
code: WAVEFRONT_COMPUTE_WGSL
|
|
3032
|
+
});
|
|
3033
|
+
const pipelines = {
|
|
3034
|
+
prepareMeshTrianglesAndLeaves: await createComputePipeline(
|
|
3035
|
+
device,
|
|
3036
|
+
computeShader,
|
|
3037
|
+
accelerationPipelineLayout,
|
|
3038
|
+
"prepareMeshTrianglesAndLeaves",
|
|
3039
|
+
"plasius.wavefront.prepareMeshTrianglesAndLeaves"
|
|
3040
|
+
),
|
|
3041
|
+
sortBvhLeafRefs: await createComputePipeline(
|
|
3042
|
+
device,
|
|
3043
|
+
computeShader,
|
|
3044
|
+
accelerationPipelineLayout,
|
|
3045
|
+
"sortBvhLeafRefs",
|
|
3046
|
+
"plasius.wavefront.sortBvhLeafRefs"
|
|
3047
|
+
),
|
|
3048
|
+
writeSortedBvhLeaves: await createComputePipeline(
|
|
3049
|
+
device,
|
|
3050
|
+
computeShader,
|
|
3051
|
+
accelerationPipelineLayout,
|
|
3052
|
+
"writeSortedBvhLeaves",
|
|
3053
|
+
"plasius.wavefront.writeSortedBvhLeaves"
|
|
3054
|
+
),
|
|
3055
|
+
buildBvhInternalLevel: await createComputePipeline(
|
|
3056
|
+
device,
|
|
3057
|
+
computeShader,
|
|
3058
|
+
accelerationPipelineLayout,
|
|
3059
|
+
"buildBvhInternalLevel",
|
|
3060
|
+
"plasius.wavefront.buildBvhInternalLevel"
|
|
3061
|
+
),
|
|
3062
|
+
generatePrimaryRays: await createComputePipeline(
|
|
3063
|
+
device,
|
|
3064
|
+
computeShader,
|
|
3065
|
+
tracePipelineLayout,
|
|
3066
|
+
"generatePrimaryRays",
|
|
3067
|
+
"plasius.wavefront.generatePrimaryRays"
|
|
3068
|
+
),
|
|
3069
|
+
intersectActiveQueue: await createComputePipeline(
|
|
3070
|
+
device,
|
|
3071
|
+
computeShader,
|
|
3072
|
+
tracePipelineLayout,
|
|
3073
|
+
"intersectActiveQueue",
|
|
3074
|
+
"plasius.wavefront.intersectActiveQueue"
|
|
3075
|
+
),
|
|
3076
|
+
resolveSurfaceRecords: await createComputePipeline(
|
|
3077
|
+
device,
|
|
3078
|
+
computeShader,
|
|
3079
|
+
tracePipelineLayout,
|
|
3080
|
+
"resolveSurfaceRecords",
|
|
3081
|
+
"plasius.wavefront.resolveSurfaceRecords"
|
|
3082
|
+
),
|
|
3083
|
+
compactAndSwapQueues: await createComputePipeline(
|
|
3084
|
+
device,
|
|
3085
|
+
computeShader,
|
|
3086
|
+
tracePipelineLayout,
|
|
3087
|
+
"compactAndSwapQueues",
|
|
3088
|
+
"plasius.wavefront.compactAndSwapQueues"
|
|
3089
|
+
),
|
|
3090
|
+
accumulateTerminalRadiance: await createComputePipeline(
|
|
3091
|
+
device,
|
|
3092
|
+
computeShader,
|
|
3093
|
+
tracePipelineLayout,
|
|
3094
|
+
"accumulateTerminalRadiance",
|
|
3095
|
+
"plasius.wavefront.accumulateTerminalRadiance"
|
|
3096
|
+
),
|
|
3097
|
+
denoiseLinearRadiance: await createComputePipeline(
|
|
3098
|
+
device,
|
|
3099
|
+
computeShader,
|
|
3100
|
+
denoiseRadiancePipelineLayout,
|
|
3101
|
+
"denoiseLinearRadiance",
|
|
3102
|
+
"plasius.wavefront.denoiseLinearRadiance"
|
|
3103
|
+
),
|
|
3104
|
+
resolveDenoisedOutputImage: await createComputePipeline(
|
|
3105
|
+
device,
|
|
3106
|
+
computeShader,
|
|
3107
|
+
denoiseResolvePipelineLayout,
|
|
3108
|
+
"resolveDenoisedOutputImage",
|
|
3109
|
+
"plasius.wavefront.resolveDenoisedOutputImage"
|
|
3110
|
+
)
|
|
3111
|
+
};
|
|
3112
|
+
function createTraceBindGroup(activeBuffer, nextBuffer, label, frameConfigBuffer = configBuffer) {
|
|
3113
|
+
return device.createBindGroup({
|
|
3114
|
+
label,
|
|
3115
|
+
layout: traceBindGroupLayout,
|
|
3116
|
+
entries: [
|
|
3117
|
+
{ binding: 0, resource: { buffer: activeBuffer } },
|
|
3118
|
+
{ binding: 1, resource: { buffer: nextBuffer } },
|
|
3119
|
+
{ binding: 2, resource: { buffer: hitBuffer } },
|
|
3120
|
+
{ binding: 3, resource: { buffer: accumulationBuffer } },
|
|
3121
|
+
{ binding: 4, resource: { buffer: sceneObjectBuffer } },
|
|
3122
|
+
{ binding: 5, resource: { buffer: frameConfigBuffer, size: CONFIG_BUFFER_BYTES } },
|
|
3123
|
+
{ binding: 6, resource: { buffer: counterBuffer } },
|
|
3124
|
+
{ binding: 7, resource: outputView },
|
|
3125
|
+
{ binding: 8, resource: { buffer: triangleBuffer } },
|
|
3126
|
+
{ binding: 9, resource: { buffer: bvhNodeBuffer } },
|
|
3127
|
+
{ binding: 16, resource: radianceView },
|
|
3128
|
+
{ binding: 19, resource: { buffer: environmentPortalBuffer } }
|
|
3129
|
+
]
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
const bindGroups = [
|
|
3133
|
+
createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
|
|
3134
|
+
createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
|
|
3135
|
+
];
|
|
3136
|
+
const bvhBuildBindGroup = device.createBindGroup({
|
|
3137
|
+
label: "plasius.wavefront.bind.bvhBuild",
|
|
3138
|
+
layout: accelerationBindGroupLayout,
|
|
3139
|
+
entries: [
|
|
3140
|
+
{ binding: 5, resource: { buffer: bvhBuildConfigBuffer, size: CONFIG_BUFFER_BYTES } },
|
|
3141
|
+
{ binding: 8, resource: { buffer: triangleBuffer } },
|
|
3142
|
+
{ binding: 9, resource: { buffer: bvhNodeBuffer } },
|
|
3143
|
+
{ binding: 10, resource: { buffer: meshVertexBuffer } },
|
|
3144
|
+
{ binding: 11, resource: { buffer: meshIndexBuffer } },
|
|
3145
|
+
{ binding: 12, resource: { buffer: meshRangeBuffer } },
|
|
3146
|
+
{ binding: 13, resource: { buffer: bvhLeafRefBuffer } }
|
|
3147
|
+
]
|
|
3148
|
+
});
|
|
3149
|
+
function createDenoiseRadianceBindGroup(inputView, targetView, label) {
|
|
3150
|
+
return device.createBindGroup({
|
|
3151
|
+
label,
|
|
3152
|
+
layout: denoiseRadianceBindGroupLayout,
|
|
3153
|
+
entries: [
|
|
3154
|
+
{ binding: 5, resource: { buffer: configBuffer, size: CONFIG_BUFFER_BYTES } },
|
|
3155
|
+
{ binding: 14, resource: inputView },
|
|
3156
|
+
{ binding: 15, resource: targetView }
|
|
3157
|
+
]
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
function createDenoiseResolveBindGroup(inputView, targetView, label) {
|
|
3161
|
+
return device.createBindGroup({
|
|
3162
|
+
label,
|
|
3163
|
+
layout: denoiseResolveBindGroupLayout,
|
|
3164
|
+
entries: [
|
|
3165
|
+
{ binding: 5, resource: { buffer: configBuffer, size: CONFIG_BUFFER_BYTES } },
|
|
3166
|
+
{ binding: 17, resource: inputView },
|
|
3167
|
+
{ binding: 18, resource: targetView }
|
|
3168
|
+
]
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
const denoiseRadianceBindGroup = createDenoiseRadianceBindGroup(
|
|
3172
|
+
radianceView,
|
|
3173
|
+
denoiseScratchView,
|
|
3174
|
+
"plasius.wavefront.bind.denoise.radianceToScratch"
|
|
3175
|
+
);
|
|
3176
|
+
const denoiseResolveBindGroup = createDenoiseResolveBindGroup(
|
|
3177
|
+
denoiseScratchView,
|
|
3178
|
+
outputView,
|
|
3179
|
+
"plasius.wavefront.bind.denoise.scratchToOutput"
|
|
3180
|
+
);
|
|
3181
|
+
const presentBindGroupLayout = device.createBindGroupLayout({
|
|
3182
|
+
label: "plasius.wavefront.presentBindGroupLayout",
|
|
3183
|
+
entries: [
|
|
3184
|
+
{ binding: 0, visibility: constants.shader.FRAGMENT, texture: { sampleType: "float" } },
|
|
3185
|
+
{ binding: 1, visibility: constants.shader.FRAGMENT, sampler: { type: "filtering" } }
|
|
3186
|
+
]
|
|
3187
|
+
});
|
|
3188
|
+
const presentShader = device.createShaderModule({
|
|
3189
|
+
label: "plasius.wavefront.presentShader",
|
|
3190
|
+
code: PRESENT_WGSL
|
|
3191
|
+
});
|
|
3192
|
+
const presentPipeline = await createRenderPipeline(device, {
|
|
3193
|
+
label: "plasius.wavefront.presentPipeline",
|
|
3194
|
+
layout: device.createPipelineLayout({
|
|
3195
|
+
label: "plasius.wavefront.presentPipelineLayout",
|
|
3196
|
+
bindGroupLayouts: [presentBindGroupLayout]
|
|
3197
|
+
}),
|
|
3198
|
+
vertex: { module: presentShader, entryPoint: "vertexMain" },
|
|
3199
|
+
fragment: {
|
|
3200
|
+
module: presentShader,
|
|
3201
|
+
entryPoint: "fragmentMain",
|
|
3202
|
+
targets: [{ format }]
|
|
3203
|
+
},
|
|
3204
|
+
primitive: { topology: "triangle-list" }
|
|
3205
|
+
});
|
|
3206
|
+
const presentBindGroup = device.createBindGroup({
|
|
3207
|
+
label: "plasius.wavefront.presentBindGroup",
|
|
3208
|
+
layout: presentBindGroupLayout,
|
|
3209
|
+
entries: [
|
|
3210
|
+
{ binding: 0, resource: outputView },
|
|
3211
|
+
{ binding: 1, resource: sampler }
|
|
3212
|
+
]
|
|
3213
|
+
});
|
|
3214
|
+
let frame = 0;
|
|
3215
|
+
let accelerationBuilt = !config.gpuAccelerationBuildRequired;
|
|
3216
|
+
let accelerationBuildCount = 0;
|
|
3217
|
+
function createFrameConfigWriter(frameIndex) {
|
|
3218
|
+
let slot = 0;
|
|
3219
|
+
return (tile, buildRange = {}) => {
|
|
3220
|
+
if (slot >= frameConfigSlotCount) {
|
|
3221
|
+
throw new Error("Wavefront frame config slot capacity exceeded.");
|
|
3222
|
+
}
|
|
3223
|
+
const offset = slot * configBufferStride;
|
|
3224
|
+
slot += 1;
|
|
3225
|
+
device.queue.writeBuffer(
|
|
3226
|
+
configBuffer,
|
|
3227
|
+
offset,
|
|
3228
|
+
createConfigPayload(config, tile, frameIndex, buildRange)
|
|
3229
|
+
);
|
|
3230
|
+
return offset;
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
function dispatchGpuAccelerationBuild(frameIndex) {
|
|
3234
|
+
if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
|
|
3235
|
+
return false;
|
|
3236
|
+
}
|
|
3237
|
+
const buildTile = tiles[0] ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
3238
|
+
const encoder = device.createCommandEncoder({
|
|
3239
|
+
label: `plasius.wavefront.buildAcceleration.${frameIndex}`
|
|
3240
|
+
});
|
|
3241
|
+
device.queue.writeBuffer(
|
|
3242
|
+
bvhBuildConfigBuffer,
|
|
3243
|
+
0,
|
|
3244
|
+
createConfigPayload(config, buildTile, frameIndex, {
|
|
3245
|
+
sortItemCount: config.bvhLeafSortCapacity
|
|
3246
|
+
})
|
|
3247
|
+
);
|
|
3248
|
+
config.bvhSortStages.forEach((sortStage, stageIndex) => {
|
|
3249
|
+
device.queue.writeBuffer(
|
|
3250
|
+
bvhBuildConfigBuffer,
|
|
3251
|
+
(stageIndex + 1) * configBufferStride,
|
|
3252
|
+
createConfigPayload(config, buildTile, frameIndex, {
|
|
3253
|
+
start: sortStage.compareDistance,
|
|
3254
|
+
count: sortStage.sequenceSize,
|
|
3255
|
+
sortItemCount: config.bvhLeafSortCapacity
|
|
3256
|
+
})
|
|
3257
|
+
);
|
|
3258
|
+
});
|
|
3259
|
+
const buildLevelConfigStart = 1 + config.bvhSortStages.length;
|
|
3260
|
+
config.bvhBuildLevels.forEach((buildLevel, levelIndex) => {
|
|
3261
|
+
device.queue.writeBuffer(
|
|
3262
|
+
bvhBuildConfigBuffer,
|
|
3263
|
+
(buildLevelConfigStart + levelIndex) * configBufferStride,
|
|
3264
|
+
createConfigPayload(config, buildTile, frameIndex, buildLevel)
|
|
3265
|
+
);
|
|
3266
|
+
});
|
|
3267
|
+
const passEncoder = encoder.beginComputePass({
|
|
3268
|
+
label: "plasius.wavefront.buildAccelerationPass"
|
|
3269
|
+
});
|
|
3270
|
+
passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
|
|
3271
|
+
passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
|
|
3272
|
+
passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
|
|
3273
|
+
passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
|
|
3274
|
+
for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
|
|
3275
|
+
passEncoder.setBindGroup(0, bvhBuildBindGroup, [
|
|
3276
|
+
(stageIndex + 1) * configBufferStride
|
|
3277
|
+
]);
|
|
3278
|
+
passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
|
|
3279
|
+
}
|
|
3280
|
+
passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
|
|
3281
|
+
passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
|
|
3282
|
+
passEncoder.dispatchWorkgroups(Math.ceil(config.triangleCount / WORKGROUP_SIZE));
|
|
3283
|
+
passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
|
|
3284
|
+
for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
|
|
3285
|
+
const buildLevel = config.bvhBuildLevels[levelIndex];
|
|
3286
|
+
passEncoder.setBindGroup(0, bvhBuildBindGroup, [
|
|
3287
|
+
(buildLevelConfigStart + levelIndex) * configBufferStride
|
|
3288
|
+
]);
|
|
3289
|
+
passEncoder.dispatchWorkgroups(Math.ceil(buildLevel.count / WORKGROUP_SIZE));
|
|
3290
|
+
}
|
|
3291
|
+
passEncoder.end();
|
|
3292
|
+
device.queue.submit([encoder.finish()]);
|
|
3293
|
+
accelerationBuilt = true;
|
|
3294
|
+
accelerationBuildCount += 1;
|
|
3295
|
+
return true;
|
|
3296
|
+
}
|
|
3297
|
+
function encodeTileSample(encoder, tile, configOffset) {
|
|
3298
|
+
const passEncoder = encoder.beginComputePass({
|
|
3299
|
+
label: "plasius.wavefront.computePass"
|
|
3300
|
+
});
|
|
3301
|
+
const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
|
|
3302
|
+
const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
|
|
3303
|
+
passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
|
|
3304
|
+
passEncoder.setPipeline(pipelines.generatePrimaryRays);
|
|
3305
|
+
passEncoder.dispatchWorkgroups(tileWorkgroups);
|
|
3306
|
+
for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
|
|
3307
|
+
passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
|
|
3308
|
+
passEncoder.setPipeline(pipelines.intersectActiveQueue);
|
|
3309
|
+
passEncoder.dispatchWorkgroups(capacityWorkgroups);
|
|
3310
|
+
passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
|
|
3311
|
+
passEncoder.dispatchWorkgroups(capacityWorkgroups);
|
|
3312
|
+
passEncoder.setPipeline(pipelines.compactAndSwapQueues);
|
|
3313
|
+
passEncoder.dispatchWorkgroups(1);
|
|
3314
|
+
}
|
|
3315
|
+
passEncoder.end();
|
|
3316
|
+
}
|
|
3317
|
+
function encodeTileOutput(encoder, tile, configOffset) {
|
|
3318
|
+
const passEncoder = encoder.beginComputePass({
|
|
3319
|
+
label: "plasius.wavefront.outputPass"
|
|
3320
|
+
});
|
|
3321
|
+
const tileWorkgroups = Math.ceil(tile.width * tile.height / WORKGROUP_SIZE);
|
|
3322
|
+
passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
|
|
3323
|
+
passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
|
|
3324
|
+
passEncoder.dispatchWorkgroups(tileWorkgroups);
|
|
3325
|
+
passEncoder.end();
|
|
3326
|
+
}
|
|
3327
|
+
function encodeDenoise(encoder, configOffset) {
|
|
3328
|
+
if (!config.denoise) {
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
const radiancePass = encoder.beginComputePass({
|
|
3332
|
+
label: "plasius.wavefront.denoiseRadiancePass"
|
|
3333
|
+
});
|
|
3334
|
+
radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
|
|
3335
|
+
radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
|
|
3336
|
+
radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
|
|
3337
|
+
radiancePass.end();
|
|
3338
|
+
const resolvePass = encoder.beginComputePass({
|
|
3339
|
+
label: "plasius.wavefront.denoiseResolvePass"
|
|
3340
|
+
});
|
|
3341
|
+
resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
|
|
3342
|
+
resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
|
|
3343
|
+
resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
|
|
3344
|
+
resolvePass.end();
|
|
3345
|
+
}
|
|
3346
|
+
function encodePresent(encoder) {
|
|
3347
|
+
const texture = context.getCurrentTexture();
|
|
3348
|
+
const passEncoder = encoder.beginRenderPass({
|
|
3349
|
+
label: "plasius.wavefront.presentPass",
|
|
3350
|
+
colorAttachments: [
|
|
3351
|
+
{
|
|
3352
|
+
view: texture.createView(),
|
|
3353
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
3354
|
+
loadOp: "clear",
|
|
3355
|
+
storeOp: "store"
|
|
3356
|
+
}
|
|
3357
|
+
]
|
|
3358
|
+
});
|
|
3359
|
+
passEncoder.setPipeline(presentPipeline);
|
|
3360
|
+
passEncoder.setBindGroup(0, presentBindGroup);
|
|
3361
|
+
passEncoder.draw(3);
|
|
3362
|
+
passEncoder.end();
|
|
3363
|
+
}
|
|
3364
|
+
function dispatchFrame(frameIndex) {
|
|
3365
|
+
const writeFrameConfig = createFrameConfigWriter(frameIndex);
|
|
3366
|
+
const encoder = device.createCommandEncoder({
|
|
3367
|
+
label: `plasius.wavefront.frame.${frameIndex}.batched`
|
|
3368
|
+
});
|
|
3369
|
+
for (const tile of tiles) {
|
|
3370
|
+
for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
|
|
3371
|
+
const configOffset = writeFrameConfig(tile, {
|
|
3372
|
+
sampleIndex,
|
|
3373
|
+
sampleWeight: 1 / config.samplesPerPixel
|
|
3374
|
+
});
|
|
3375
|
+
encodeTileSample(encoder, tile, configOffset);
|
|
3376
|
+
}
|
|
3377
|
+
const outputConfigOffset = writeFrameConfig(tile, {
|
|
3378
|
+
sampleIndex: 0,
|
|
3379
|
+
sampleWeight: 1 / config.samplesPerPixel
|
|
3380
|
+
});
|
|
3381
|
+
encodeTileOutput(encoder, tile, outputConfigOffset);
|
|
3382
|
+
}
|
|
3383
|
+
if (config.denoise) {
|
|
3384
|
+
const denoiseConfigOffset = writeFrameConfig(
|
|
3385
|
+
{ x: 0, y: 0, width: config.width, height: config.height },
|
|
3386
|
+
{ sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
|
|
3387
|
+
);
|
|
3388
|
+
encodeDenoise(encoder, denoiseConfigOffset);
|
|
3389
|
+
}
|
|
3390
|
+
encodePresent(encoder);
|
|
3391
|
+
device.queue.submit([encoder.finish()]);
|
|
3392
|
+
return 1;
|
|
3393
|
+
}
|
|
3394
|
+
function renderOnce() {
|
|
3395
|
+
frame += 1;
|
|
3396
|
+
const frameIndex = frame + config.frameIndex;
|
|
3397
|
+
const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
|
|
3398
|
+
const frameSubmissionCount = dispatchFrame(frameIndex);
|
|
3399
|
+
return Object.freeze({
|
|
3400
|
+
frame,
|
|
3401
|
+
width: config.width,
|
|
3402
|
+
height: config.height,
|
|
3403
|
+
maxDepth: config.maxDepth,
|
|
3404
|
+
tiles: tiles.length,
|
|
3405
|
+
tileSize: config.tileSize,
|
|
3406
|
+
samplesPerPixel: config.samplesPerPixel,
|
|
3407
|
+
screenRays: config.width * config.height,
|
|
3408
|
+
primaryRays: config.width * config.height * config.samplesPerPixel,
|
|
3409
|
+
sceneObjectCount: config.sceneObjectCount,
|
|
3410
|
+
triangleCount: config.triangleCount,
|
|
3411
|
+
emissiveTriangleCount: config.emissiveTriangleCount,
|
|
3412
|
+
environmentPortalCount: config.environmentPortalCount,
|
|
3413
|
+
environmentPortalMode: config.environmentPortalMode,
|
|
3414
|
+
bvhNodeCount: config.bvhNodeCount,
|
|
3415
|
+
displayQuality: config.displayQuality,
|
|
3416
|
+
accelerationBuildMode: config.accelerationBuildMode,
|
|
3417
|
+
gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
|
|
3418
|
+
accelerationBuildSubmitted,
|
|
3419
|
+
accelerationBuilt,
|
|
3420
|
+
accelerationBuildCount,
|
|
3421
|
+
commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
|
|
3422
|
+
frameConfigSlots: frameConfigSlotCount,
|
|
3423
|
+
memory: config.memory
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
async function readOutputProbe(optionsForProbe = {}) {
|
|
3427
|
+
const mapMode = constants.map;
|
|
3428
|
+
if (!mapMode) {
|
|
3429
|
+
throw new Error("GPUMapMode.READ is unavailable in this environment.");
|
|
3430
|
+
}
|
|
3431
|
+
const x = clamp(readNonNegativeInteger("x", optionsForProbe.x, Math.floor(config.width / 2)), 0, config.width - 1);
|
|
3432
|
+
const y = clamp(readNonNegativeInteger("y", optionsForProbe.y, Math.floor(config.height / 2)), 0, config.height - 1);
|
|
3433
|
+
const readback = device.createBuffer({
|
|
3434
|
+
label: "plasius.wavefront.outputProbe",
|
|
3435
|
+
size: 256,
|
|
3436
|
+
usage: constants.buffer.COPY_DST | constants.buffer.MAP_READ
|
|
3437
|
+
});
|
|
3438
|
+
const encoder = device.createCommandEncoder({
|
|
3439
|
+
label: "plasius.wavefront.outputProbe.copy"
|
|
3440
|
+
});
|
|
3441
|
+
encoder.copyTextureToBuffer(
|
|
3442
|
+
{ texture: outputTexture, origin: { x, y } },
|
|
3443
|
+
{ buffer: readback, bytesPerRow: 256, rowsPerImage: 1 },
|
|
3444
|
+
{ width: 1, height: 1, depthOrArrayLayers: 1 }
|
|
3445
|
+
);
|
|
3446
|
+
device.queue.submit([encoder.finish()]);
|
|
3447
|
+
await readback.mapAsync(mapMode.READ);
|
|
3448
|
+
const bytes = new Uint8Array(readback.getMappedRange()).slice(0, 4);
|
|
3449
|
+
readback.unmap();
|
|
3450
|
+
readback.destroy?.();
|
|
3451
|
+
return Object.freeze({
|
|
3452
|
+
x,
|
|
3453
|
+
y,
|
|
3454
|
+
rgba: Object.freeze(Array.from(bytes)),
|
|
3455
|
+
luminance: (0.2126 * bytes[0] + 0.7152 * bytes[1] + 0.0722 * bytes[2]) / 255
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
function updateSceneObjects(sceneObjects) {
|
|
3459
|
+
const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
|
|
3460
|
+
packedScene = nextPackedScene;
|
|
3461
|
+
config = createWavefrontPathTracingComputeConfig({
|
|
3462
|
+
...options,
|
|
3463
|
+
canvas,
|
|
3464
|
+
width: config.width,
|
|
3465
|
+
height: config.height,
|
|
3466
|
+
maxDepth: config.maxDepth,
|
|
3467
|
+
tileSize: config.tileSize,
|
|
3468
|
+
samplesPerPixel: config.samplesPerPixel,
|
|
3469
|
+
sceneObjectCapacity: config.sceneObjectCapacity,
|
|
3470
|
+
sceneObjects: packedScene.objects,
|
|
3471
|
+
frameIndex: config.frameIndex
|
|
3472
|
+
});
|
|
3473
|
+
device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
|
|
3474
|
+
return config;
|
|
3475
|
+
}
|
|
3476
|
+
function getSnapshot() {
|
|
3477
|
+
return Object.freeze({
|
|
3478
|
+
frame,
|
|
3479
|
+
width: config.width,
|
|
3480
|
+
height: config.height,
|
|
3481
|
+
maxDepth: config.maxDepth,
|
|
3482
|
+
tiles: tiles.length,
|
|
3483
|
+
tileSize: config.tileSize,
|
|
3484
|
+
samplesPerPixel: config.samplesPerPixel,
|
|
3485
|
+
sceneObjectCount: config.sceneObjectCount,
|
|
3486
|
+
triangleCount: config.triangleCount,
|
|
3487
|
+
emissiveTriangleCount: config.emissiveTriangleCount,
|
|
3488
|
+
environmentPortalCount: config.environmentPortalCount,
|
|
3489
|
+
environmentPortalMode: config.environmentPortalMode,
|
|
3490
|
+
bvhNodeCount: config.bvhNodeCount,
|
|
3491
|
+
displayQuality: config.displayQuality,
|
|
3492
|
+
accelerationBuildMode: config.accelerationBuildMode,
|
|
3493
|
+
gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
|
|
3494
|
+
accelerationBuilt,
|
|
3495
|
+
accelerationBuildCount,
|
|
3496
|
+
frameConfigSlots: frameConfigSlotCount,
|
|
3497
|
+
memory: config.memory
|
|
3498
|
+
});
|
|
3499
|
+
}
|
|
3500
|
+
function destroy() {
|
|
3501
|
+
activeQueue.destroy?.();
|
|
3502
|
+
nextQueue.destroy?.();
|
|
3503
|
+
hitBuffer.destroy?.();
|
|
3504
|
+
accumulationBuffer.destroy?.();
|
|
3505
|
+
sceneObjectBuffer.destroy?.();
|
|
3506
|
+
triangleBuffer.destroy?.();
|
|
3507
|
+
bvhNodeBuffer.destroy?.();
|
|
3508
|
+
meshVertexBuffer.destroy?.();
|
|
3509
|
+
meshIndexBuffer.destroy?.();
|
|
3510
|
+
meshRangeBuffer.destroy?.();
|
|
3511
|
+
environmentPortalBuffer.destroy?.();
|
|
3512
|
+
bvhLeafRefBuffer.destroy?.();
|
|
3513
|
+
configBuffer.destroy?.();
|
|
3514
|
+
bvhBuildConfigBuffer.destroy?.();
|
|
3515
|
+
counterBuffer.destroy?.();
|
|
3516
|
+
radianceTexture.destroy?.();
|
|
3517
|
+
denoiseScratchTexture.destroy?.();
|
|
3518
|
+
outputTexture.destroy?.();
|
|
3519
|
+
context.unconfigure?.();
|
|
3520
|
+
}
|
|
3521
|
+
return Object.freeze({
|
|
3522
|
+
canvas,
|
|
3523
|
+
context,
|
|
3524
|
+
device,
|
|
3525
|
+
format,
|
|
3526
|
+
config,
|
|
3527
|
+
renderOnce,
|
|
3528
|
+
readOutputProbe,
|
|
3529
|
+
updateSceneObjects,
|
|
3530
|
+
getSnapshot,
|
|
3531
|
+
destroy
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
|
|
1
3535
|
// src/index.js
|
|
2
3536
|
var DEFAULT_CLEAR_COLOR = Object.freeze([0.07, 0.11, 0.18, 1]);
|
|
3
3537
|
var DEFAULT_CANVAS_SELECTOR = "canvas[data-plasius-gpu-renderer]";
|
|
@@ -236,9 +3770,9 @@ function buildWavefrontBounceSchedule(maxDepth) {
|
|
|
236
3770
|
);
|
|
237
3771
|
}
|
|
238
3772
|
function createWavefrontPathTracingPlan(options = {}) {
|
|
239
|
-
const maxDepth = options.maxDepth === void 0 ? 6 :
|
|
240
|
-
const queueCapacity = options.queueCapacity === void 0 ? 8192 :
|
|
241
|
-
const accumulationResetEpoch = options.accumulationResetEpoch === void 0 ? 0 :
|
|
3773
|
+
const maxDepth = options.maxDepth === void 0 ? 6 : readPositiveInteger2("maxDepth", options.maxDepth);
|
|
3774
|
+
const queueCapacity = options.queueCapacity === void 0 ? 8192 : readPositiveInteger2("queueCapacity", options.queueCapacity);
|
|
3775
|
+
const accumulationResetEpoch = options.accumulationResetEpoch === void 0 ? 0 : readNonNegativeInteger2("accumulationResetEpoch", options.accumulationResetEpoch);
|
|
242
3776
|
const explicitLightSampling = options.explicitLightSampling === true;
|
|
243
3777
|
return Object.freeze({
|
|
244
3778
|
schemaVersion: rendererWavefrontBufferSchemaVersion,
|
|
@@ -845,13 +4379,13 @@ function readPositiveNumber(name, value) {
|
|
|
845
4379
|
}
|
|
846
4380
|
return value;
|
|
847
4381
|
}
|
|
848
|
-
function
|
|
4382
|
+
function readPositiveInteger2(name, value) {
|
|
849
4383
|
if (!Number.isInteger(value) || value <= 0) {
|
|
850
4384
|
throw new Error(`${name} must be a positive integer.`);
|
|
851
4385
|
}
|
|
852
4386
|
return value;
|
|
853
4387
|
}
|
|
854
|
-
function
|
|
4388
|
+
function readNonNegativeInteger2(name, value) {
|
|
855
4389
|
if (!Number.isInteger(value) || value < 0) {
|
|
856
4390
|
throw new Error(`${name} must be a non-negative integer.`);
|
|
857
4391
|
}
|
|
@@ -967,7 +4501,7 @@ function readDocument(documentOverride) {
|
|
|
967
4501
|
}
|
|
968
4502
|
return doc;
|
|
969
4503
|
}
|
|
970
|
-
function
|
|
4504
|
+
function resolveCanvas2(canvasOrSelector, documentOverride) {
|
|
971
4505
|
if (canvasOrSelector && typeof canvasOrSelector === "object") {
|
|
972
4506
|
return canvasOrSelector;
|
|
973
4507
|
}
|
|
@@ -1045,7 +4579,7 @@ async function createGpuRenderer(options = {}) {
|
|
|
1045
4579
|
throw new Error("Unable to obtain GPU adapter.");
|
|
1046
4580
|
}
|
|
1047
4581
|
const device = await adapter.requestDevice();
|
|
1048
|
-
const targetCanvas =
|
|
4582
|
+
const targetCanvas = resolveCanvas2(canvas, documentOverride);
|
|
1049
4583
|
const context = targetCanvas.getContext?.("webgpu");
|
|
1050
4584
|
if (!context) {
|
|
1051
4585
|
throw new Error("Unable to obtain WebGPU canvas context.");
|
|
@@ -1285,14 +4819,28 @@ function bindRendererToXrManager(renderer, xrManager, options = {}) {
|
|
|
1285
4819
|
var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
|
|
1286
4820
|
export {
|
|
1287
4821
|
bindRendererToXrManager,
|
|
4822
|
+
createDefaultWavefrontSceneObjects,
|
|
1288
4823
|
createGpuRenderer,
|
|
1289
4824
|
createRayTracingRenderPlan,
|
|
1290
4825
|
createRendererDebugHooks,
|
|
4826
|
+
createWavefrontBvhBuildLevels,
|
|
4827
|
+
createWavefrontBvhSortStages,
|
|
4828
|
+
createWavefrontEmissiveTriangleIndexSource,
|
|
4829
|
+
createWavefrontGpuMeshSource,
|
|
4830
|
+
createWavefrontMeshAcceleration,
|
|
4831
|
+
createWavefrontPathTracingComputeConfig,
|
|
4832
|
+
createWavefrontPathTracingComputeRenderer,
|
|
1291
4833
|
createWavefrontPathTracingPlan,
|
|
1292
4834
|
defaultRendererClearColor,
|
|
1293
4835
|
defaultRendererWorkerProfile,
|
|
4836
|
+
estimateWavefrontPathTracingMemory,
|
|
1294
4837
|
getRendererWorkerManifest,
|
|
1295
4838
|
getRendererWorkerProfile,
|
|
4839
|
+
normalizeWavefrontMesh,
|
|
4840
|
+
normalizeWavefrontSceneObject,
|
|
4841
|
+
packWavefrontBvhNodes,
|
|
4842
|
+
packWavefrontSceneObjects,
|
|
4843
|
+
packWavefrontTriangles,
|
|
1296
4844
|
rendererAccelerationStructureUpdateClasses,
|
|
1297
4845
|
rendererDebugOwner,
|
|
1298
4846
|
rendererRayTracingStageOrder,
|
|
@@ -1305,6 +4853,10 @@ export {
|
|
|
1305
4853
|
rendererWorkerProfileNames,
|
|
1306
4854
|
rendererWorkerProfiles,
|
|
1307
4855
|
rendererWorkerQueueClass,
|
|
1308
|
-
|
|
4856
|
+
supportsWavefrontPathTracingCompute,
|
|
4857
|
+
supportsWebGpu,
|
|
4858
|
+
wavefrontMaterialKinds,
|
|
4859
|
+
wavefrontPathTracingComputeLimits,
|
|
4860
|
+
wavefrontSceneObjectKinds
|
|
1309
4861
|
};
|
|
1310
4862
|
//# sourceMappingURL=index.js.map
|