@tinypoly/gpu-renderer 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +31 -3
- package/README.md +11 -3
- package/dist/denoise.d.ts +10 -1
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +4 -0
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +8 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +4 -1
- package/dist/frameLayout.js.map +1 -1
- package/dist/imageResolve.d.ts +2 -1
- package/dist/imageResolve.d.ts.map +1 -1
- package/dist/imageResolve.js +1 -0
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lensPass.d.ts +4 -0
- package/dist/lensPass.d.ts.map +1 -1
- package/dist/lensPass.js +14 -1
- package/dist/lensPass.js.map +1 -1
- package/dist/lights.d.ts +18 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +45 -0
- package/dist/lights.js.map +1 -1
- package/dist/oidnPass.d.ts +48 -0
- package/dist/oidnPass.d.ts.map +1 -0
- package/dist/oidnPass.js +255 -0
- package/dist/oidnPass.js.map +1 -0
- package/dist/pathTracePass.d.ts +2 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +9 -9
- package/dist/pathTracePass.js.map +1 -1
- package/dist/presentPass.d.ts +2 -0
- package/dist/presentPass.d.ts.map +1 -1
- package/dist/presentPass.js +1 -0
- package/dist/presentPass.js.map +1 -1
- package/dist/renderer.d.ts +16 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +111 -15
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +4 -0
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +3 -0
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +23 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +13 -4
- package/dist/settings.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +1 -1
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lens.wgsl.js +108 -1
- package/dist/wgsl/lens.wgsl.js.map +1 -1
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +147 -17
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +123 -91
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/present.wgsl.d.ts +1 -1
- package/dist/wgsl/present.wgsl.d.ts.map +1 -1
- package/dist/wgsl/present.wgsl.js +11 -1
- package/dist/wgsl/present.wgsl.js.map +1 -1
- package/package.json +4 -2
- package/src/denoise.ts +15 -1
- package/src/device.ts +9 -0
- package/src/frameLayout.ts +4 -1
- package/src/imageResolve.ts +2 -0
- package/src/index.ts +2 -1
- package/src/lensPass.ts +17 -1
- package/src/lights.ts +60 -0
- package/src/oidnPass.ts +284 -0
- package/src/oidnWebgpu.d.ts +5 -0
- package/src/pathTracePass.ts +11 -9
- package/src/presentPass.ts +3 -0
- package/src/renderer.ts +133 -16
- package/src/sceneUpload.ts +7 -0
- package/src/settings.ts +32 -4
- package/src/wgsl/lens.wgsl.ts +108 -1
- package/src/wgsl/lights.wgsl.ts +147 -17
- package/src/wgsl/pathTrace.wgsl.ts +123 -91
- package/src/wgsl/present.wgsl.ts +11 -1
- package/weights/LICENSE.txt +202 -0
- package/weights/rt_hdr_alb_nrm.tza +0 -0
package/src/wgsl/lights.wgsl.ts
CHANGED
|
@@ -64,8 +64,8 @@ const NO_LIGHT: u32 = 0xffffffffu;
|
|
|
64
64
|
* angle the node's box takes from the point (Conty Estevez and Kulla's importance). A point inside the box rules
|
|
65
65
|
* nothing out. A zero normal is a receiver that takes light from everywhere, such as a point in a volume.
|
|
66
66
|
*/
|
|
67
|
-
fn lightNodeImportance(node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
|
|
68
|
-
let at =
|
|
67
|
+
fn lightNodeImportance(tree: u32, node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
|
|
68
|
+
let at = tree + node * TREE_NODE_WORDS;
|
|
69
69
|
let low = extraVec4(at);
|
|
70
70
|
let high = extraVec4(at + 4u);
|
|
71
71
|
|
|
@@ -125,13 +125,13 @@ fn lightNodeImportance(node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32
|
|
|
125
125
|
return low.w * orientation * facing / distance2;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/** The chance of stepping from a node of
|
|
129
|
-
fn lightChildOdds(left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {
|
|
128
|
+
/** The chance of stepping from a node of a tree (the lights' or the windows') to each of its children, from here. */
|
|
129
|
+
fn lightChildOdds(tree: u32, left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {
|
|
130
130
|
var importance = vec2<f32>(0.0);
|
|
131
131
|
|
|
132
132
|
// One loop over the two children, so the importance is written into the kernel once per walk.
|
|
133
133
|
for (var k = 0u; k < 2u; k = k + 1u) {
|
|
134
|
-
importance[k] = lightNodeImportance(select(right, left, k == 0u), position, normal);
|
|
134
|
+
importance[k] = lightNodeImportance(tree, select(right, left, k == 0u), position, normal);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
let total = importance.x + importance.y;
|
|
@@ -139,20 +139,23 @@ fn lightChildOdds(left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>)
|
|
|
139
139
|
return select(importance / total, vec2<f32>(0.0), total <= 0.0);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
142
|
+
/**
|
|
143
|
+
* An entry picked down a tree by importance from this point, with the chance of picking it; NO_LIGHT for none. The
|
|
144
|
+
* tree is the lights' or the windows', given by where its nodes and its leaves' list start.
|
|
145
|
+
*/
|
|
146
|
+
fn sampleLightTree(tree: u32, list: u32, position: vec3<f32>, normal: vec3<f32>, u: f32, pdf: ptr<function, f32>,
|
|
144
147
|
density: ptr<function, f32>) -> u32 {
|
|
145
148
|
var node = 0u;
|
|
146
149
|
var chance = 1.0;
|
|
147
150
|
var x = u;
|
|
148
151
|
|
|
149
152
|
for (var depth = 0u; depth < 64u; depth = depth + 1u) {
|
|
150
|
-
let at =
|
|
153
|
+
let at = tree + node * TREE_NODE_WORDS;
|
|
151
154
|
let first = textures[at + 12u];
|
|
152
155
|
|
|
153
156
|
// A leaf lists its lights, too close together to tell apart from here: one is picked by its share of the power.
|
|
154
157
|
if ((first & TREE_LEAF) != 0u) {
|
|
155
|
-
let start =
|
|
158
|
+
let start = list + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;
|
|
156
159
|
let count = textures[at + 13u];
|
|
157
160
|
var slot = start + (count - 1u) * TREE_ENTRY_WORDS;
|
|
158
161
|
|
|
@@ -170,7 +173,7 @@ fn sampleLightTree(position: vec3<f32>, normal: vec3<f32>, u: f32, pdf: ptr<func
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
let second = textures[at + 13u];
|
|
173
|
-
let odds = lightChildOdds(first, second, position, normal);
|
|
176
|
+
let odds = lightChildOdds(tree, first, second, position, normal);
|
|
174
177
|
|
|
175
178
|
if (odds.x + odds.y <= 0.0) {
|
|
176
179
|
return NO_LIGHT;
|
|
@@ -208,7 +211,7 @@ fn lightTreePdf(index: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
|
|
|
208
211
|
|
|
209
212
|
let at = frame.lightTree + parent * TREE_NODE_WORDS;
|
|
210
213
|
let first = textures[at + 12u];
|
|
211
|
-
let odds = lightChildOdds(first, textures[at + 13u], position, normal);
|
|
214
|
+
let odds = lightChildOdds(frame.lightTree, first, textures[at + 13u], position, normal);
|
|
212
215
|
|
|
213
216
|
chance = chance * select(odds.y, odds.x, node == first);
|
|
214
217
|
node = parent;
|
|
@@ -252,6 +255,119 @@ fn lightPdfOfTriangle(triangle: u32, position: vec3<f32>, normal: vec3<f32>) ->
|
|
|
252
255
|
return 0.0;
|
|
253
256
|
}
|
|
254
257
|
|
|
258
|
+
/** Share of the sky's samples aimed through the windows, from a point inside a room. */
|
|
259
|
+
const PORTAL_SHARE: f32 = 0.5;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* How often the sky is aimed at through the windows from this point: from inside a room, where something stands above
|
|
263
|
+
* it, half the time; outside never, where the open sky is better found by its brightness. A function of the point alone,
|
|
264
|
+
* so the heuristic can ask it again when a bounce ray escapes.
|
|
265
|
+
*/
|
|
266
|
+
fn portalChance(position: vec3<f32>) -> f32 {
|
|
267
|
+
if (!PORTALS || frame.portalCount == 0u) {
|
|
268
|
+
return 0.0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return select(0.0, PORTAL_SHARE, traceAny(position, vec3<f32>(0.0, 1.0, 0.0), 1e30) != NO_HIT);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Window subtrees a ray's walk through the windows' tree keeps waiting. */
|
|
275
|
+
const PORTAL_STACK: u32 = 32u;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The solid angle density with which aiming through the windows produces this direction: over every window triangle
|
|
279
|
+
* it crosses, the chance the tree picks that window from here times the area density of a point on it, over the area
|
|
280
|
+
* it shows from here. The ray walks the tree as it would a hierarchy, by the nodes' boxes, carrying the chance of
|
|
281
|
+
* reaching each node, so the windows it misses cost a box test and the ones it crosses no walk back up.
|
|
282
|
+
*/
|
|
283
|
+
fn portalPdf(position: vec3<f32>, direction: vec3<f32>) -> f32 {
|
|
284
|
+
var nodes: array<u32, PORTAL_STACK>;
|
|
285
|
+
var chances: array<f32, PORTAL_STACK>;
|
|
286
|
+
var top = 1u;
|
|
287
|
+
let inverse = 1.0 / direction;
|
|
288
|
+
var pdf = 0.0;
|
|
289
|
+
|
|
290
|
+
nodes[0] = 0u;
|
|
291
|
+
chances[0] = 1.0;
|
|
292
|
+
|
|
293
|
+
loop {
|
|
294
|
+
if (top == 0u) {
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
top = top - 1u;
|
|
299
|
+
|
|
300
|
+
let node = nodes[top];
|
|
301
|
+
let chance = chances[top];
|
|
302
|
+
let at = frame.portals + node * TREE_NODE_WORDS;
|
|
303
|
+
|
|
304
|
+
if (enterBox(extraVec4(at).xyz, extraVec4(at + 4u).xyz, position, inverse, 1e30) >= 1e30) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let first = textures[at + 12u];
|
|
309
|
+
let second = textures[at + 13u];
|
|
310
|
+
|
|
311
|
+
if ((first & TREE_LEAF) != 0u) {
|
|
312
|
+
let start = frame.portalList + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;
|
|
313
|
+
|
|
314
|
+
for (var j = 0u; j < second; j = j + 1u) {
|
|
315
|
+
let slot = start + j * TREE_ENTRY_WORDS;
|
|
316
|
+
let triangle = textures[slot];
|
|
317
|
+
let t = intersectTriangle(leafOf(triangle), position, direction).x;
|
|
318
|
+
|
|
319
|
+
if (t > 0.0) {
|
|
320
|
+
let cosine = abs(dot(triangleNormal(triangle), direction));
|
|
321
|
+
|
|
322
|
+
pdf = pdf + chance * extraFloat(slot + 1u) * extraFloat(slot + 2u) * t * t / max(cosine, 1e-6);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let odds = lightChildOdds(frame.portals, first, second, position, vec3<f32>(0.0));
|
|
330
|
+
|
|
331
|
+
for (var k = 0u; k < 2u; k = k + 1u) {
|
|
332
|
+
if (odds[k] > 0.0 && top < PORTAL_STACK) {
|
|
333
|
+
nodes[top] = select(second, first, k == 0u);
|
|
334
|
+
chances[top] = chance * odds[k];
|
|
335
|
+
top = top + 1u;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return pdf;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** A direction through a point on a window the tree picks from here, or none from a point on the window itself. */
|
|
344
|
+
fn samplePortal(position: vec3<f32>, u: vec2<f32>) -> vec3<f32> {
|
|
345
|
+
var chance = 0.0;
|
|
346
|
+
var density = 0.0;
|
|
347
|
+
let triangle = sampleLightTree(frame.portals, frame.portalList, position, vec3<f32>(0.0), u.x, &chance, &density);
|
|
348
|
+
|
|
349
|
+
if (triangle == NO_LIGHT) {
|
|
350
|
+
return vec3<f32>(0.0);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// The tree spent the sample walking down; a fresh one places the point, which the pick does not depend on.
|
|
354
|
+
let point = trianglePointAt(triangle, triangleBarycentric(vec2<f32>(fract(u.x * 4096.0), u.y)));
|
|
355
|
+
let offset = point - position;
|
|
356
|
+
|
|
357
|
+
return select(vec3<f32>(0.0), normalize(offset), dot(offset, offset) > 1e-12);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** The density of the sky's sampling from this point for this direction: by brightness, through windows, or both. */
|
|
361
|
+
fn skyPdf(position: vec3<f32>, direction: vec3<f32>, share: f32) -> f32 {
|
|
362
|
+
var pdf = (1.0 - share) * environmentPdf(direction);
|
|
363
|
+
|
|
364
|
+
if (share > 0.0) {
|
|
365
|
+
pdf = pdf + share * portalPdf(position, direction);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return pdf;
|
|
369
|
+
}
|
|
370
|
+
|
|
255
371
|
/** The power heuristic: how much of a contribution the strategy that produced it keeps, against the other one. */
|
|
256
372
|
fn misWeight(pdf: f32, other: f32) -> f32 {
|
|
257
373
|
let a = pdf * pdf;
|
|
@@ -307,7 +423,7 @@ fn sampleLight(position: vec3<f32>, normal: vec3<f32>, pick: f32, u: vec2<f32>)
|
|
|
307
423
|
if (frame.lightTree != 0u && pick < frame.localLightPick) {
|
|
308
424
|
var chance = 0.0;
|
|
309
425
|
var density = 1.0;
|
|
310
|
-
let index = sampleLightTree(position, normal, pick / frame.localLightPick, &chance, &density);
|
|
426
|
+
let index = sampleLightTree(frame.lightTree, frame.lightList, position, normal, pick / frame.localLightPick, &chance, &density);
|
|
311
427
|
|
|
312
428
|
if (index == NO_LIGHT || chance <= 0.0) {
|
|
313
429
|
return noLight();
|
|
@@ -356,17 +472,31 @@ fn sampleLightEntry(light: Light, pdf: f32, position: vec3<f32>, u: vec2<f32>) -
|
|
|
356
472
|
sample.misPdf = 0.0;
|
|
357
473
|
|
|
358
474
|
if (light.kind == LIGHT_ENVIRONMENT) {
|
|
359
|
-
|
|
475
|
+
// From inside a room, part of the time through a window: daylight comes in there or not at all.
|
|
476
|
+
let share = portalChance(position);
|
|
477
|
+
var direction = vec3<f32>(0.0);
|
|
478
|
+
|
|
479
|
+
if (u.x < share) {
|
|
480
|
+
direction = samplePortal(position, vec2<f32>(u.x / share, u.y));
|
|
481
|
+
} else {
|
|
482
|
+
let drawn = sampleEnvironmentDirection(vec2<f32>(min((u.x - share) / (1.0 - share), 0.99999994), u.y));
|
|
483
|
+
|
|
484
|
+
if (drawn.pdf > 0.0) {
|
|
485
|
+
direction = drawn.direction;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let mixed = skyPdf(position, direction, share);
|
|
360
490
|
|
|
361
|
-
if (
|
|
491
|
+
if (all(direction == vec3<f32>(0.0)) || mixed <= 0.0) {
|
|
362
492
|
return noLight();
|
|
363
493
|
}
|
|
364
494
|
|
|
365
|
-
let density = pdf *
|
|
495
|
+
let density = pdf * mixed;
|
|
366
496
|
|
|
367
|
-
sample.direction =
|
|
497
|
+
sample.direction = direction;
|
|
368
498
|
sample.distance = 1e30;
|
|
369
|
-
sample.value = environmentRadiance(
|
|
499
|
+
sample.value = environmentRadiance(direction) / density;
|
|
370
500
|
// A bounce ray that escapes finds the same light, so the two strategies share it.
|
|
371
501
|
sample.misPdf = density;
|
|
372
502
|
|
|
@@ -42,7 +42,9 @@ fn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {
|
|
|
42
42
|
return frame.background.rgb;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// A procedural sky paints the background
|
|
45
|
+
// A procedural sky paints the background with the strength it lights with, and its foreground over it. The CPU
|
|
46
|
+
// renderer leaves that strength out of what the camera sees, which a renderer exposing for the light cannot: the sky
|
|
47
|
+
// that lights a wall five times brighter than itself would show as a dark band behind it.
|
|
46
48
|
if (frame.skyProgram != NO_PROGRAM || frame.skyIntensity > 0.0) {
|
|
47
49
|
if (frame.skyBackground == 0u) {
|
|
48
50
|
return vec3<f32>(0.0);
|
|
@@ -54,10 +56,10 @@ fn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {
|
|
|
54
56
|
input.direction = direction;
|
|
55
57
|
input.diffuseColor = vec4<f32>(1.0);
|
|
56
58
|
|
|
57
|
-
return runProgram(frame.skyProgram, input).color.rgb;
|
|
59
|
+
return runProgram(frame.skyProgram, input).color.rgb * frame.skyIntensity;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
return environmentRadiance(direction)
|
|
62
|
+
return environmentRadiance(direction);
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
return environmentRadiance(direction);
|
|
@@ -67,7 +69,7 @@ fn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {
|
|
|
67
69
|
* How much of the sky a bounce ray that escaped keeps. Direct sampling aims at the bright parts of an image too, so the
|
|
68
70
|
* two share it; when direct sampling took all of it, as it does from a toon surface, the bounce keeps none.
|
|
69
71
|
*/
|
|
70
|
-
fn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool) -> f32 {
|
|
72
|
+
fn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool, scatteredAt: vec3<f32>) -> f32 {
|
|
71
73
|
if (frame.environmentPick <= 0.0) {
|
|
72
74
|
return 1.0;
|
|
73
75
|
}
|
|
@@ -80,7 +82,8 @@ fn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool) -> f3
|
|
|
80
82
|
return 1.0;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
// The sky was aimed at from the vertex the bounce left, through its windows as well when that vertex is inside.
|
|
86
|
+
return misWeight(bsdfPdf, frame.environmentPick * skyPdf(scatteredAt, direction, portalChance(scatteredAt)));
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
/** Scales a contribution down to the luminance ceiling, which trades a little bias for the fireflies it removes. */
|
|
@@ -548,7 +551,7 @@ fn traceRadiance(
|
|
|
548
551
|
if (escapeMap != NO_TEXTURE) {
|
|
549
552
|
sky = sky + sampleEquirect(escapeMap, direction).rgb * escapeIntensity;
|
|
550
553
|
} else {
|
|
551
|
-
sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect);
|
|
554
|
+
sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect, lastPosition);
|
|
552
555
|
}
|
|
553
556
|
|
|
554
557
|
let contribution = throughput * sky;
|
|
@@ -703,8 +706,14 @@ fn traceRadiance(
|
|
|
703
706
|
depth = length(point - rayOrigin);
|
|
704
707
|
}
|
|
705
708
|
|
|
709
|
+
// A window the camera ray goes straight through is not what the camera sees: what lies behind it is, so the
|
|
710
|
+
// denoiser takes its guides there, and the air its distance. Glass has next to no diffuse colour and one flat
|
|
711
|
+
// normal, which would leave everything seen through it unfiltered. The window's own guides stand until then, for
|
|
712
|
+
// the paths that reflect off it instead.
|
|
713
|
+
let window = surface.transmission > 0.0 && surface.thickness <= 0.0;
|
|
714
|
+
|
|
706
715
|
if (straight && !guided) {
|
|
707
|
-
guided =
|
|
716
|
+
guided = !window;
|
|
708
717
|
diffuseAlbedo = diffuseGuide(surface);
|
|
709
718
|
specularAlbedoGuide = select(specularAlbedo(surface, -direction), vec3<f32>(1.0), (surface.flags & MATERIAL_LIT) == 0u);
|
|
710
719
|
guideNormal = surface.normal;
|
|
@@ -745,7 +754,7 @@ fn traceRadiance(
|
|
|
745
754
|
}
|
|
746
755
|
}
|
|
747
756
|
|
|
748
|
-
if (!hazed && frame.atmosphereColor.w > 0.0) {
|
|
757
|
+
if (!hazed && !window && frame.atmosphereColor.w > 0.0) {
|
|
749
758
|
hazed = true;
|
|
750
759
|
// From the eye, where the camera ray only starts on the near plane; an orthographic view has no eye.
|
|
751
760
|
hazeDistance = length(point - select(frame.cameraToWorld[3].xyz, rayOrigin, frame.cameraKind == 1u));
|
|
@@ -765,7 +774,11 @@ fn traceRadiance(
|
|
|
765
774
|
}
|
|
766
775
|
|
|
767
776
|
// Three does not light a basic, points, line or sprite material, so nothing carries on from one.
|
|
768
|
-
|
|
777
|
+
// Past the last bounce a path still crosses a window straight on, which scatters nothing, to find the sky behind
|
|
778
|
+
// it: cut there, the daylight a room gets through its glazing would go missing from every last bounce.
|
|
779
|
+
let spent = bounce >= frame.bounces;
|
|
780
|
+
|
|
781
|
+
if ((surface.flags & MATERIAL_LIT) == 0u || (spent && !window)) {
|
|
769
782
|
break;
|
|
770
783
|
}
|
|
771
784
|
|
|
@@ -788,8 +801,105 @@ fn traceRadiance(
|
|
|
788
801
|
var nextOrigin = point + direction * RAY_EPSILON;
|
|
789
802
|
|
|
790
803
|
// Transmission replaces part of the diffuse layer, so that share of the time the surface is a dielectric instead.
|
|
791
|
-
// Choosing it in proportion to its weight leaves both halves unweighted
|
|
792
|
-
|
|
804
|
+
// Choosing it in proportion to its weight leaves both halves unweighted. The opaque half aims at a light; on a
|
|
805
|
+
// window that lets only part of the light through, it does so on every path, weighed by its share, rather than on
|
|
806
|
+
// the paths that chose it: a sunlit pane seen against a dim room would otherwise be lit on one path in five and
|
|
807
|
+
// dark on the rest, which is the grain glass shows.
|
|
808
|
+
let dielectric = surface.transmission > 0.0 && lobe.y < surface.transmission;
|
|
809
|
+
let windowLight = window && !spent && surface.transmission < 1.0 && surface.subsurface <= 0.0;
|
|
810
|
+
|
|
811
|
+
if (!dielectric || windowLight) {
|
|
812
|
+
var chance = 0.0;
|
|
813
|
+
|
|
814
|
+
if (!dielectric) {
|
|
815
|
+
// Light that enters the material: a random walk inside it from here, in place of the share of the diffuse
|
|
816
|
+
// layer that scatters inside. Chosen by chance, so neither branch is weighted against the other.
|
|
817
|
+
// The window's opaque share is a surface like any other, which a spent path stops at.
|
|
818
|
+
if (spent) {
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (SUBSURFACE_WALK) {
|
|
823
|
+
chance = subsurfaceChance(surface, view);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (SUBSURFACE_WALK && lobe.x < chance) {
|
|
827
|
+
let medium = materials[surface.material].subsurface;
|
|
828
|
+
|
|
829
|
+
throughput = throughput
|
|
830
|
+
* ((1.0 - surface.metalness) * surface.subsurface * coatTransmittance(surface, view) / chance);
|
|
831
|
+
sigma = vec3<f32>(1.0) / max(medium.rgb, vec3<f32>(1e-6));
|
|
832
|
+
walkAlbedo = subsurfaceAlbedo(surface.albedo);
|
|
833
|
+
walkG = materials[surface.material].subsurfaceAnisotropy;
|
|
834
|
+
walkSteps = 0u;
|
|
835
|
+
|
|
836
|
+
// Light that scattered inside is diffuse light, which is the signal it feeds.
|
|
837
|
+
if (!split) {
|
|
838
|
+
split = true;
|
|
839
|
+
splitBase = radiance;
|
|
840
|
+
splitDiffuse = true;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
inMedium = true;
|
|
844
|
+
primary = false;
|
|
845
|
+
straight = false;
|
|
846
|
+
bsdfPdf = 0.0;
|
|
847
|
+
lightsOwnDirect = false;
|
|
848
|
+
escapeMap = NO_TEXTURE;
|
|
849
|
+
bounce = bounce + 1u;
|
|
850
|
+
direction = cosineDirection(-surface.facing, bounceSample);
|
|
851
|
+
origin = point - surface.facing * RAY_EPSILON;
|
|
852
|
+
limit = walkDistance(throughput, sigma, pick, &walkOdds);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
throughput = throughput / (1.0 - chance);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
let position = point + surface.normal * RAY_EPSILON;
|
|
860
|
+
var directDiffuse: vec3<f32>;
|
|
861
|
+
let lit = select(throughput, throughput * (1.0 - surface.transmission), windowLight);
|
|
862
|
+
let direct = lit * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse,
|
|
863
|
+
select(0.0, volumeDensity, inVolume));
|
|
864
|
+
let added = select(direct, clampIndirect(direct), bounce > 0u);
|
|
865
|
+
let before = radiance;
|
|
866
|
+
|
|
867
|
+
radiance = radiance + added;
|
|
868
|
+
|
|
869
|
+
if (!dielectric) {
|
|
870
|
+
let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);
|
|
871
|
+
|
|
872
|
+
// The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.
|
|
873
|
+
if (!split) {
|
|
874
|
+
split = true;
|
|
875
|
+
splitBase = before;
|
|
876
|
+
splitDirect = added;
|
|
877
|
+
splitDirectDiffuse = lit * directDiffuse * select(1.0, luminanceOf(added) / max(luminanceOf(direct), 1e-20),
|
|
878
|
+
bounce > 0u);
|
|
879
|
+
splitDiffuse = !scattered.valid || scattered.diffuse;
|
|
880
|
+
waitingForSpecular = scattered.valid && !scattered.diffuse;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (!scattered.valid) {
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
throughput = throughput * scattered.weight;
|
|
888
|
+
bsdfPdf = scattered.pdf;
|
|
889
|
+
lastPosition = position;
|
|
890
|
+
lastNormal = treeNormal(surface);
|
|
891
|
+
lightsOwnDirect = surface.kind == KIND_TOON;
|
|
892
|
+
escapeMap = surface.envMap;
|
|
893
|
+
escapeIntensity = surface.envMapIntensity;
|
|
894
|
+
next = scattered.direction;
|
|
895
|
+
nextOrigin = position;
|
|
896
|
+
primary = false;
|
|
897
|
+
straight = false;
|
|
898
|
+
bounce = bounce + 1u;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (dielectric) {
|
|
793
903
|
var ior = surface.ior;
|
|
794
904
|
|
|
795
905
|
// Three's dispersion: each colour refracts with its own index, spread around the material's. The path picks one
|
|
@@ -809,7 +919,8 @@ fn traceRadiance(
|
|
|
809
919
|
|
|
810
920
|
let crossing = sampleDielectric(surface, view, ior, lobe.y / surface.transmission, bounceSample);
|
|
811
921
|
|
|
812
|
-
|
|
922
|
+
// A reflection off the glass is a scattering, which a spent path no longer has.
|
|
923
|
+
if (!crossing.valid || (spent && !crossing.straight)) {
|
|
813
924
|
break;
|
|
814
925
|
}
|
|
815
926
|
|
|
@@ -844,85 +955,6 @@ fn traceRadiance(
|
|
|
844
955
|
if (!primary) {
|
|
845
956
|
bounce = bounce + 1u;
|
|
846
957
|
}
|
|
847
|
-
} else {
|
|
848
|
-
// Light that enters the material: a random walk inside it from here, in place of the share of the diffuse
|
|
849
|
-
// layer that scatters inside. Chosen by chance, so neither branch is weighted against the other.
|
|
850
|
-
var chance = 0.0;
|
|
851
|
-
|
|
852
|
-
if (SUBSURFACE_WALK) {
|
|
853
|
-
chance = subsurfaceChance(surface, view);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
if (SUBSURFACE_WALK && lobe.x < chance) {
|
|
857
|
-
let medium = materials[surface.material].subsurface;
|
|
858
|
-
|
|
859
|
-
throughput = throughput
|
|
860
|
-
* ((1.0 - surface.metalness) * surface.subsurface * coatTransmittance(surface, view) / chance);
|
|
861
|
-
sigma = vec3<f32>(1.0) / max(medium.rgb, vec3<f32>(1e-6));
|
|
862
|
-
walkAlbedo = subsurfaceAlbedo(surface.albedo);
|
|
863
|
-
walkG = materials[surface.material].subsurfaceAnisotropy;
|
|
864
|
-
walkSteps = 0u;
|
|
865
|
-
|
|
866
|
-
// Light that scattered inside is diffuse light, which is the signal it feeds.
|
|
867
|
-
if (!split) {
|
|
868
|
-
split = true;
|
|
869
|
-
splitBase = radiance;
|
|
870
|
-
splitDiffuse = true;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
inMedium = true;
|
|
874
|
-
primary = false;
|
|
875
|
-
straight = false;
|
|
876
|
-
bsdfPdf = 0.0;
|
|
877
|
-
lightsOwnDirect = false;
|
|
878
|
-
escapeMap = NO_TEXTURE;
|
|
879
|
-
bounce = bounce + 1u;
|
|
880
|
-
direction = cosineDirection(-surface.facing, bounceSample);
|
|
881
|
-
origin = point - surface.facing * RAY_EPSILON;
|
|
882
|
-
limit = walkDistance(throughput, sigma, pick, &walkOdds);
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
throughput = throughput / (1.0 - chance);
|
|
887
|
-
|
|
888
|
-
let position = point + surface.normal * RAY_EPSILON;
|
|
889
|
-
var directDiffuse: vec3<f32>;
|
|
890
|
-
let direct = throughput * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse,
|
|
891
|
-
select(0.0, volumeDensity, inVolume));
|
|
892
|
-
let added = select(direct, clampIndirect(direct), bounce > 0u);
|
|
893
|
-
let before = radiance;
|
|
894
|
-
|
|
895
|
-
radiance = radiance + added;
|
|
896
|
-
|
|
897
|
-
let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);
|
|
898
|
-
|
|
899
|
-
// The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.
|
|
900
|
-
if (!split) {
|
|
901
|
-
split = true;
|
|
902
|
-
splitBase = before;
|
|
903
|
-
splitDirect = added;
|
|
904
|
-
splitDirectDiffuse = throughput * directDiffuse * select(1.0, luminanceOf(added) / max(luminanceOf(direct), 1e-20),
|
|
905
|
-
bounce > 0u);
|
|
906
|
-
splitDiffuse = !scattered.valid || scattered.diffuse;
|
|
907
|
-
waitingForSpecular = scattered.valid && !scattered.diffuse;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (!scattered.valid) {
|
|
911
|
-
break;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
throughput = throughput * scattered.weight;
|
|
915
|
-
bsdfPdf = scattered.pdf;
|
|
916
|
-
lastPosition = position;
|
|
917
|
-
lastNormal = treeNormal(surface);
|
|
918
|
-
lightsOwnDirect = surface.kind == KIND_TOON;
|
|
919
|
-
escapeMap = surface.envMap;
|
|
920
|
-
escapeIntensity = surface.envMapIntensity;
|
|
921
|
-
next = scattered.direction;
|
|
922
|
-
nextOrigin = position;
|
|
923
|
-
primary = false;
|
|
924
|
-
straight = false;
|
|
925
|
-
bounce = bounce + 1u;
|
|
926
958
|
}
|
|
927
959
|
|
|
928
960
|
// A path that carries almost nothing is ended by chance rather than by a cut, which keeps the estimate unbiased.
|
package/src/wgsl/present.wgsl.ts
CHANGED
|
@@ -15,6 +15,8 @@ struct Present {
|
|
|
15
15
|
transparent: u32,
|
|
16
16
|
// Whether the colour comes from the denoiser's output rather than straight from the accumulation.
|
|
17
17
|
denoised: u32,
|
|
18
|
+
// How much of AgX's Punchy look goes over its base curve.
|
|
19
|
+
contrast: f32,
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
@group(0) @binding(0) var<uniform> present: Present;
|
|
@@ -63,8 +65,16 @@ fn toneMapAgx(color: vec3<f32>) -> vec3<f32> {
|
|
|
63
65
|
vec3<f32>(-0.0728, -0.0083, 1.1187));
|
|
64
66
|
let logged = inset * (toAgx * color);
|
|
65
67
|
let x = clamp((log2(max(logged, vec3<f32>(1e-10))) + 12.47393) / 16.499999, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
66
|
-
let
|
|
68
|
+
let base = 15.5 * pow(x, vec3<f32>(6.0)) - 40.14 * pow(x, vec3<f32>(5.0)) + 31.96 * pow(x, vec3<f32>(4.0))
|
|
67
69
|
- 6.868 * pow(x, vec3<f32>(3.0)) + 0.4298 * x * x + 0.1191 * x - 0.00232;
|
|
70
|
+
// A look in the spirit of Blender's Punchy, a power of 1.35 and more saturation, but pivoted on middle grey: Punchy
|
|
71
|
+
// raises the power around zero, which takes middle grey from half the display down to a third and darkens the whole
|
|
72
|
+
// image. Here the highlights rise and the shadows fall while grey, which the automatic exposure aims at, stands.
|
|
73
|
+
let pivot = 0.49673;
|
|
74
|
+
let contrasted = pivot * pow(max(base, vec3<f32>(0.0)) / pivot, vec3<f32>(1.35));
|
|
75
|
+
let luma = dot(contrasted, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
76
|
+
let punchy = luma + 1.2 * (contrasted - luma);
|
|
77
|
+
let curved = mix(base, punchy, clamp(present.contrast, 0.0, 1.0));
|
|
68
78
|
|
|
69
79
|
return fromAgx * pow(max(outset * curved, vec3<f32>(0.0)), vec3<f32>(2.2));
|
|
70
80
|
}
|