three-realtime-rt 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +559 -394
- package/package.json +52 -52
- package/src/CompositePass.js +261 -214
- package/src/CopyPass.js +75 -0
- package/src/DenoisePass.js +221 -202
- package/src/GBufferPass.js +290 -228
- package/src/RTLightingPass.js +1124 -680
- package/src/RealtimeRaytracer.js +1106 -730
- package/src/RestirPass.js +31 -4
- package/src/SceneCompiler.js +542 -424
- package/src/TAAPass.js +271 -230
- package/src/VolumetricPass.js +398 -329
- package/src/blueNoise.js +17 -17
- package/src/index.d.ts +107 -6
package/src/RTLightingPass.js
CHANGED
|
@@ -1,680 +1,1124 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import { shaderStructs, shaderIntersectFunction } from "three-mesh-bvh";
|
|
3
|
-
import { MAX_LIGHTS } from "./SceneCompiler.js";
|
|
4
|
-
import { SKY_GLSL } from "./sky.glsl.js";
|
|
5
|
-
import { BVH_ANY_HIT_GLSL } from "./bvhAnyHit.glsl.js";
|
|
6
|
-
|
|
7
|
-
const fullscreenVert = /* glsl */ `
|
|
8
|
-
out vec2 vUv;
|
|
9
|
-
void main() {
|
|
10
|
-
vUv = uv;
|
|
11
|
-
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
12
|
-
}
|
|
13
|
-
`;
|
|
14
|
-
|
|
15
|
-
const rtLightingFrag = /* glsl */ `
|
|
16
|
-
precision highp float;
|
|
17
|
-
precision highp isampler2D;
|
|
18
|
-
precision highp usampler2D;
|
|
19
|
-
|
|
20
|
-
${shaderStructs}
|
|
21
|
-
${shaderIntersectFunction}
|
|
22
|
-
${BVH_ANY_HIT_GLSL}
|
|
23
|
-
${SKY_GLSL}
|
|
24
|
-
|
|
25
|
-
#define MAX_LIGHTS ${MAX_LIGHTS}
|
|
26
|
-
#define PI 3.14159265358979
|
|
27
|
-
|
|
28
|
-
layout(location = 0) out vec4 outIrradiance;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
uniform BVH
|
|
35
|
-
uniform
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
uniform sampler2D
|
|
40
|
-
uniform sampler2D
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
uniform sampler2D
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
uniform sampler2D
|
|
50
|
-
uniform sampler2D
|
|
51
|
-
uniform
|
|
52
|
-
uniform mat4
|
|
53
|
-
uniform
|
|
54
|
-
uniform
|
|
55
|
-
uniform
|
|
56
|
-
uniform
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
uniform vec4
|
|
60
|
-
uniform
|
|
61
|
-
uniform
|
|
62
|
-
uniform
|
|
63
|
-
uniform
|
|
64
|
-
uniform
|
|
65
|
-
uniform bool
|
|
66
|
-
uniform bool
|
|
67
|
-
|
|
68
|
-
uniform
|
|
69
|
-
uniform
|
|
70
|
-
uniform
|
|
71
|
-
uniform
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
uniform
|
|
77
|
-
uniform
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
uniform
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
vec3
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
vec3
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
//
|
|
336
|
-
vec3
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
vec3
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
vec3
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
float
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { shaderStructs, shaderIntersectFunction } from "three-mesh-bvh";
|
|
3
|
+
import { MAX_LIGHTS } from "./SceneCompiler.js";
|
|
4
|
+
import { SKY_GLSL } from "./sky.glsl.js";
|
|
5
|
+
import { BVH_ANY_HIT_GLSL } from "./bvhAnyHit.glsl.js";
|
|
6
|
+
|
|
7
|
+
const fullscreenVert = /* glsl */ `
|
|
8
|
+
out vec2 vUv;
|
|
9
|
+
void main() {
|
|
10
|
+
vUv = uv;
|
|
11
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const rtLightingFrag = /* glsl */ `
|
|
16
|
+
precision highp float;
|
|
17
|
+
precision highp isampler2D;
|
|
18
|
+
precision highp usampler2D;
|
|
19
|
+
|
|
20
|
+
${shaderStructs}
|
|
21
|
+
${shaderIntersectFunction}
|
|
22
|
+
${BVH_ANY_HIT_GLSL}
|
|
23
|
+
${SKY_GLSL}
|
|
24
|
+
|
|
25
|
+
#define MAX_LIGHTS ${MAX_LIGHTS}
|
|
26
|
+
#define PI 3.14159265358979
|
|
27
|
+
|
|
28
|
+
layout(location = 0) out vec4 outIrradiance;
|
|
29
|
+
layout(location = 1) out vec4 outSpecular; // dielectric direct specular (fresh, this frame)
|
|
30
|
+
|
|
31
|
+
in vec2 vUv;
|
|
32
|
+
|
|
33
|
+
// Two-level BVH: static (uploaded once) + dynamic (small, refit each frame).
|
|
34
|
+
uniform BVH bvhStatic;
|
|
35
|
+
uniform BVH bvhDynamic;
|
|
36
|
+
uniform bool uHasDynamic;
|
|
37
|
+
// One packed per-vertex texture per level: normal.xyz + materialIndex.w.
|
|
38
|
+
// (Two BVH structs already use 8 samplers; WebGL2 guarantees only 16 total.)
|
|
39
|
+
uniform sampler2D uAttrStatic;
|
|
40
|
+
uniform sampler2D uAttrDynamic;
|
|
41
|
+
uniform sampler2D uMaterialsTex; // 2 texels per material (shared)
|
|
42
|
+
|
|
43
|
+
uniform sampler2D uGWorldPos;
|
|
44
|
+
uniform sampler2D uGNormalMetal;
|
|
45
|
+
|
|
46
|
+
// temporal reprojection (stage 2). Validation is plane-distance only — the
|
|
47
|
+
// normal test was dropped to free a sampler for the ReSTIR reservoir (same
|
|
48
|
+
// simplification the TAA resolve already made, no observed regressions).
|
|
49
|
+
uniform sampler2D uPrevAccum; // rgb = irradiance history, a = sample count
|
|
50
|
+
uniform sampler2D uPrevGWorldPos; // previous frame's G-buffer, for validation
|
|
51
|
+
uniform sampler2D uReservoir; // ReSTIR winner per pixel (see RestirPass)
|
|
52
|
+
uniform mat4 uPrevViewProj;
|
|
53
|
+
uniform mat4 uViewProj;
|
|
54
|
+
uniform vec3 uCameraPos;
|
|
55
|
+
uniform float uMaxHistory;
|
|
56
|
+
uniform bool uTemporalReprojection;
|
|
57
|
+
uniform float uFireflyClamp;
|
|
58
|
+
|
|
59
|
+
uniform vec4 uLightPosType[MAX_LIGHTS]; // xyz pos|dir, w: 0 point, 1 directional, >=2 spot (w-2 = cosInner)
|
|
60
|
+
uniform vec4 uLightColorRadius[MAX_LIGHTS]; // rgb color*intensity, w radius
|
|
61
|
+
uniform vec4 uLightDirCone[MAX_LIGHTS]; // spot: direction.xyz + cos(outer angle)
|
|
62
|
+
uniform int uLightCount;
|
|
63
|
+
uniform int uEmissiveCount; // NEE area-light triangles in row 1 of uMaterialsTex
|
|
64
|
+
uniform bool uEmissiveCDF; // importance-sample tris by the power CDF (row 66)
|
|
65
|
+
uniform bool uReflEnabled; // traced reflections on metallic surfaces
|
|
66
|
+
uniform bool uRefrEnabled; // traced refraction on transmissive surfaces
|
|
67
|
+
uniform bool uBlendEnabled; // straight-through view continuation on blend surfaces
|
|
68
|
+
uniform float uIor; // index of refraction for transmissive materials
|
|
69
|
+
uniform bool uLightStochastic; // 1 direct shadow ray/pixel/frame instead of 1/light
|
|
70
|
+
uniform bool uRestirEnabled; // shade the reservoir winner instead of sampling
|
|
71
|
+
uniform bool uGIHalfRate; // GI ray on alternating checkerboard, doubled
|
|
72
|
+
|
|
73
|
+
uniform vec3 uEnvColor;
|
|
74
|
+
uniform float uEnvIntensity;
|
|
75
|
+
uniform float uFrame;
|
|
76
|
+
uniform float uEps;
|
|
77
|
+
uniform bool uGIEnabled;
|
|
78
|
+
|
|
79
|
+
// Procedural sky (when enabled, replaces the flat env colour as the "miss" term
|
|
80
|
+
// for GI rays — this is what gives natural outdoor bounce light).
|
|
81
|
+
uniform bool uSkyEnabled;
|
|
82
|
+
uniform vec3 uSunDir; // direction toward the sun
|
|
83
|
+
uniform vec3 uSunColor;
|
|
84
|
+
uniform vec3 uSkyZenith;
|
|
85
|
+
uniform vec3 uSkyHorizon;
|
|
86
|
+
uniform float uSkyIntensity;
|
|
87
|
+
|
|
88
|
+
// ---------- RNG ----------
|
|
89
|
+
// The FIRST four random numbers each frame come from a 64x64 blue-noise tile
|
|
90
|
+
// (rows 2..65 of the scene-data texture), rotated over time with an R2
|
|
91
|
+
// low-discrepancy sequence. Those dimensions drive direct lighting — light
|
|
92
|
+
// pick + area-sample position — where noise is most visible; blue noise turns
|
|
93
|
+
// the residual error high-frequency, which temporal accumulation and the
|
|
94
|
+
// denoiser remove far better than white-noise clumps. Later dimensions fall
|
|
95
|
+
// back to PCG white noise (correlating many dimensions hurts).
|
|
96
|
+
uint gSeed;
|
|
97
|
+
int gBnDim;
|
|
98
|
+
vec4 gBlueNoise;
|
|
99
|
+
uint pcgHash(uint s) {
|
|
100
|
+
uint state = s * 747796405u + 2891336453u;
|
|
101
|
+
uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
|
|
102
|
+
return (word >> 22u) ^ word;
|
|
103
|
+
}
|
|
104
|
+
float rand() {
|
|
105
|
+
if (gBnDim < 4) {
|
|
106
|
+
float v = gBlueNoise[gBnDim];
|
|
107
|
+
gBnDim++;
|
|
108
|
+
return v;
|
|
109
|
+
}
|
|
110
|
+
gSeed = pcgHash(gSeed);
|
|
111
|
+
return float(gSeed) * (1.0 / 4294967296.0);
|
|
112
|
+
}
|
|
113
|
+
vec2 rand2() { return vec2(rand(), rand()); }
|
|
114
|
+
|
|
115
|
+
vec4 fetchBlueNoise() {
|
|
116
|
+
ivec2 p = ivec2(gl_FragCoord.xy) & 63;
|
|
117
|
+
vec4 bn = texelFetch(uMaterialsTex, ivec2(p.x, 2 + p.y), 0);
|
|
118
|
+
// R2 sequence: per-frame toroidal shift, decorrelated per channel.
|
|
119
|
+
vec4 shift = fract(float(uFrame) * vec4(0.6180340, 0.7548777, 0.5698403, 0.8191725));
|
|
120
|
+
return fract(bn + shift);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Branchless orthonormal basis (Duff et al. 2017) — cheaper and stable for
|
|
124
|
+
// every n, including the poles the old cross-product picker handled branchily.
|
|
125
|
+
void orthoBasis(vec3 n, out vec3 t, out vec3 b) {
|
|
126
|
+
float s = n.z >= 0.0 ? 1.0 : -1.0;
|
|
127
|
+
float a = -1.0 / (s + n.z);
|
|
128
|
+
float m = n.x * n.y * a;
|
|
129
|
+
t = vec3(1.0 + s * n.x * n.x * a, s * m, -s * n.x);
|
|
130
|
+
b = vec3(m, s + n.y * n.y * a, -n.y);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
vec3 cosineSampleHemisphere(vec3 n, vec2 u) {
|
|
134
|
+
float a = 2.0 * PI * u.x;
|
|
135
|
+
float r = sqrt(u.y);
|
|
136
|
+
vec3 t, b;
|
|
137
|
+
orthoBasis(n, t, b);
|
|
138
|
+
return normalize(t * (r * cos(a)) + b * (r * sin(a)) + n * sqrt(max(0.0, 1.0 - u.y)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
vec3 randUnitVector() {
|
|
142
|
+
vec2 u = rand2();
|
|
143
|
+
float z = u.x * 2.0 - 1.0;
|
|
144
|
+
float a = u.y * 2.0 * PI;
|
|
145
|
+
float r = sqrt(max(0.0, 1.0 - z * z));
|
|
146
|
+
return vec3(r * cos(a), r * sin(a), z);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------- two-level BVH helpers ----------
|
|
150
|
+
// Closest hit across both levels; isDyn says which one so the caller samples
|
|
151
|
+
// the matching vertex-attribute textures. (No backticks in these GLSL comments —
|
|
152
|
+
// they would terminate the enclosing JS template literal.)
|
|
153
|
+
bool traceBoth(vec3 ro, vec3 rd, out uvec4 fi, out vec3 bary, out float dist, out bool isDyn) {
|
|
154
|
+
uvec4 fiS; vec3 fnS; vec3 bcS; float sideS; float distS;
|
|
155
|
+
bool hitS = bvhIntersectFirstHit(bvhStatic, ro, rd, fiS, fnS, bcS, sideS, distS);
|
|
156
|
+
uvec4 fiD; vec3 fnD; vec3 bcD; float sideD; float distD;
|
|
157
|
+
bool hitD = uHasDynamic && bvhIntersectFirstHit(bvhDynamic, ro, rd, fiD, fnD, bcD, sideD, distD);
|
|
158
|
+
if (hitS && (!hitD || distS <= distD)) { fi = fiS; bary = bcS; dist = distS; isDyn = false; return true; }
|
|
159
|
+
if (hitD) { fi = fiD; bary = bcD; dist = distD; isDyn = true; return true; }
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Shadow rays only need to know IF something blocks, not what's closest —
|
|
164
|
+
// the unordered any-hit traversal early-outs on the first blocker.
|
|
165
|
+
bool occluded(vec3 ro, vec3 rd, float maxDist) {
|
|
166
|
+
if (bvhIntersectAnyHit(bvhStatic, ro, rd, maxDist - 2.0 * uEps)) return true;
|
|
167
|
+
if (uHasDynamic && bvhIntersectAnyHit(bvhDynamic, ro, rd, maxDist - 2.0 * uEps)) return true;
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
void fetchMaterial(float matIndex, out vec3 albedo, out float roughness,
|
|
172
|
+
out vec3 emissive, out float metalness) {
|
|
173
|
+
int mi = int(round(matIndex)) * 2;
|
|
174
|
+
vec4 t0 = texelFetch(uMaterialsTex, ivec2(mi, 0), 0);
|
|
175
|
+
vec4 t1 = texelFetch(uMaterialsTex, ivec2(mi + 1, 0), 0);
|
|
176
|
+
albedo = t0.rgb;
|
|
177
|
+
roughness = t0.a;
|
|
178
|
+
emissive = t1.rgb;
|
|
179
|
+
metalness = t1.a;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------- PBR specular (Cook-Torrance GGX) ----------
|
|
183
|
+
// A separate specular radiance is accumulated for the primary surface's DIRECT
|
|
184
|
+
// lighting alongside the demodulated diffuse irradiance. Because CompositePass
|
|
185
|
+
// multiplies the irradiance by albedo, a white dielectric highlight (F0 ~= 0.04)
|
|
186
|
+
// cannot ride in that buffer — it is emitted into gSpec and written to a second
|
|
187
|
+
// MRT attachment (added by the composite WITHOUT the albedo multiply). Metals'
|
|
188
|
+
// specular is albedo-tinted (F0 = albedo), so it stays in the reflection path
|
|
189
|
+
// where the composite's albedo multiply supplies the tint; gSpec is therefore
|
|
190
|
+
// scaled by (1 - metal)(1 - transmission) at output. Net effective Fresnel
|
|
191
|
+
// across both buffers is mix(0.04, albedo, metal) without the lighting pass ever
|
|
192
|
+
// sampling albedo (that would push it past the 16-sampler minimum).
|
|
193
|
+
vec3 gSpec; // accumulated dielectric direct specular radiance
|
|
194
|
+
vec3 gViewDir; // unit vector from the primary surface toward the camera
|
|
195
|
+
float gSpecRough; // primary surface roughness (drives the GGX lobe width)
|
|
196
|
+
bool gWantSpec; // true only while shading the PRIMARY surface's direct light
|
|
197
|
+
|
|
198
|
+
float D_GGX(float NoH, float a) {
|
|
199
|
+
float a2 = a * a;
|
|
200
|
+
float d = NoH * NoH * (a2 - 1.0) + 1.0;
|
|
201
|
+
return a2 / max(PI * d * d, 1e-8);
|
|
202
|
+
}
|
|
203
|
+
// Height-correlated Smith visibility (already folds in the 1/(4 NoL NoV) term).
|
|
204
|
+
float V_SmithGGX(float NoV, float NoL, float a) {
|
|
205
|
+
float a2 = a * a;
|
|
206
|
+
float gv = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
|
|
207
|
+
float gl = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
|
|
208
|
+
return 0.5 / max(gv + gl, 1e-5);
|
|
209
|
+
}
|
|
210
|
+
vec3 F_Schlick(float VoH, vec3 f0) {
|
|
211
|
+
return f0 + (1.0 - f0) * pow(clamp(1.0 - VoH, 0.0, 1.0), 5.0);
|
|
212
|
+
}
|
|
213
|
+
// Specular BRDF value (without the incoming NoL*radiance factor). F0 fixed at
|
|
214
|
+
// the dielectric 0.04 — metals are handled in the reflection path.
|
|
215
|
+
float ggxSpec(vec3 N, vec3 L) {
|
|
216
|
+
vec3 H = normalize(gViewDir + L);
|
|
217
|
+
float NoH = max(dot(N, H), 0.0);
|
|
218
|
+
float NoV = max(dot(N, gViewDir), 1e-4);
|
|
219
|
+
float NoL = max(dot(N, L), 1e-4);
|
|
220
|
+
float VoH = max(dot(gViewDir, H), 0.0);
|
|
221
|
+
// Clamp alpha off zero so a mirror-smooth dielectric does not produce an
|
|
222
|
+
// infinite spike the temporal buffer cannot resolve.
|
|
223
|
+
float a = max(gSpecRough * gSpecRough, 2e-3);
|
|
224
|
+
return D_GGX(NoH, a) * V_SmithGGX(NoV, NoL, a) * F_Schlick(VoH, vec3(0.04)).x;
|
|
225
|
+
}
|
|
226
|
+
// Add the dielectric specular for one light: li is the incoming radiance
|
|
227
|
+
// factor (light colour * cone / dist^2), NoL the geometric cosine.
|
|
228
|
+
void addSpec(vec3 N, vec3 L, vec3 li, float NoL) {
|
|
229
|
+
if (!gWantSpec) return;
|
|
230
|
+
gSpec += li * (NoL * ggxSpec(N, L));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------- lighting ----------
|
|
234
|
+
// Direct irradiance (demodulated: no albedo) at point P with normal N,
|
|
235
|
+
// from light i, with one shadow ray. Area-samples point lights for soft shadows.
|
|
236
|
+
// Spot cone falloff: smooth between the outer and inner cone cosines
|
|
237
|
+
// (posType.w = 2 + cosInner; dirCone.w = cosOuter).
|
|
238
|
+
float spotFalloff(int i, vec3 lightToP) {
|
|
239
|
+
vec4 posType = uLightPosType[i];
|
|
240
|
+
if (posType.w < 1.5) return 1.0;
|
|
241
|
+
vec4 dc = uLightDirCone[i];
|
|
242
|
+
return smoothstep(dc.w, posType.w - 2.0, dot(dc.xyz, lightToP));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
vec3 lightContribution(int i, vec3 P, vec3 N) {
|
|
246
|
+
vec4 posType = uLightPosType[i];
|
|
247
|
+
vec4 colRad = uLightColorRadius[i];
|
|
248
|
+
|
|
249
|
+
vec3 L;
|
|
250
|
+
float dist2 = 1.0;
|
|
251
|
+
float maxDist = 1e7;
|
|
252
|
+
float cone = 1.0;
|
|
253
|
+
|
|
254
|
+
if (posType.w < 0.5 || posType.w >= 1.5) {
|
|
255
|
+
// point/spot light: sample a point on its sphere for soft shadows
|
|
256
|
+
vec3 lp = posType.xyz + randUnitVector() * colRad.w;
|
|
257
|
+
vec3 d = lp - P;
|
|
258
|
+
float dl = length(d);
|
|
259
|
+
if (dl < 1e-5) return vec3(0.0);
|
|
260
|
+
L = d / dl;
|
|
261
|
+
dist2 = dl * dl;
|
|
262
|
+
maxDist = dl;
|
|
263
|
+
cone = spotFalloff(i, -L);
|
|
264
|
+
if (cone <= 0.0) return vec3(0.0);
|
|
265
|
+
} else {
|
|
266
|
+
// directional light: jitter within a small cone
|
|
267
|
+
L = normalize(-posType.xyz + randUnitVector() * colRad.w);
|
|
268
|
+
dist2 = 1.0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
float NdotL = dot(N, L);
|
|
272
|
+
if (NdotL <= 0.0) return vec3(0.0);
|
|
273
|
+
|
|
274
|
+
if (occluded(P + N * uEps, L, maxDist)) return vec3(0.0);
|
|
275
|
+
vec3 li = colRad.rgb * (cone / dist2);
|
|
276
|
+
addSpec(N, L, li, NdotL); // same shadow ray shadows the highlight
|
|
277
|
+
return li * NdotL;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Direct light at a GI bounce hit: sample ONE random light (weighted by count).
|
|
281
|
+
vec3 sampleOneLight(vec3 P, vec3 N) {
|
|
282
|
+
if (uLightCount == 0) return vec3(0.0);
|
|
283
|
+
int i = min(int(rand() * float(uLightCount)), uLightCount - 1);
|
|
284
|
+
return lightContribution(i, P, N) * float(uLightCount);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Next-event estimation on emissive-mesh triangles (row 1 of uMaterialsTex):
|
|
288
|
+
// pick one triangle, sample a point on it, cast one shadow ray, convert the
|
|
289
|
+
// area pdf to solid angle. Turns emitters into proper soft area lights instead
|
|
290
|
+
// of surfaces a GI ray has to hit by luck.
|
|
291
|
+
//
|
|
292
|
+
// NOISE CAVEAT: emissive NEE is the highest-variance direct-light path in the
|
|
293
|
+
// engine — one triangle sample per pixel per frame, and the area-to-solid-angle
|
|
294
|
+
// conversion carries a 1/dist^2 that spikes into fireflies when a shading point
|
|
295
|
+
// sits close to a small emitter. Two mitigations stack here:
|
|
296
|
+
// 1. uEmissiveCDF (default on): the triangle is IMPORTANCE-SAMPLED by
|
|
297
|
+
// area x emitted luminance via the power CDF in the scene-data texture
|
|
298
|
+
// (row 2 + 64 — see SceneCompiler's layout comment). A big bright panel is
|
|
299
|
+
// picked proportionally more often than a tiny dim strip, and each sample
|
|
300
|
+
// is weighted by its true pick probability — same mean, far less variance
|
|
301
|
+
// than the uniform 1-of-N pick.
|
|
302
|
+
// 2. ReSTIR reservoirs converge each pixel onto the emitter that matters
|
|
303
|
+
// (the demo keeps restir on whenever emissive NEE is on;
|
|
304
|
+
// RealtimeRaytracer.compileScene logs a hint otherwise).
|
|
305
|
+
// fireflyClamp and the denoiser absorb the residual tail. Distance-aware
|
|
306
|
+
// selection and solid-angle triangle sampling remain future work.
|
|
307
|
+
vec3 sampleEmissiveTri(vec3 P, vec3 N) {
|
|
308
|
+
if (uEmissiveCount == 0) return vec3(0.0);
|
|
309
|
+
int idx;
|
|
310
|
+
float invProb; // 1 / P(picked this triangle)
|
|
311
|
+
if (uEmissiveCDF) {
|
|
312
|
+
// Binary search the power CDF: 8 steps covers MAX_EMISSIVE_TRIS = 256.
|
|
313
|
+
float u = rand();
|
|
314
|
+
int lo = 0;
|
|
315
|
+
int hi = uEmissiveCount - 1;
|
|
316
|
+
for (int s = 0; s < 8; s++) {
|
|
317
|
+
if (lo >= hi) break;
|
|
318
|
+
int mid = (lo + hi) >> 1;
|
|
319
|
+
if (u > texelFetch(uMaterialsTex, ivec2(mid, 66), 0).x) lo = mid + 1;
|
|
320
|
+
else hi = mid;
|
|
321
|
+
}
|
|
322
|
+
idx = lo;
|
|
323
|
+
invProb = 1.0 / max(texelFetch(uMaterialsTex, ivec2(idx, 66), 0).y, 1e-8);
|
|
324
|
+
} else {
|
|
325
|
+
idx = min(int(rand() * float(uEmissiveCount)), uEmissiveCount - 1);
|
|
326
|
+
invProb = float(uEmissiveCount);
|
|
327
|
+
}
|
|
328
|
+
int i = idx * 4;
|
|
329
|
+
vec4 t0 = texelFetch(uMaterialsTex, ivec2(i, 1), 0); // v0 | area
|
|
330
|
+
vec4 t1 = texelFetch(uMaterialsTex, ivec2(i + 1, 1), 0); // e1 | emit.r
|
|
331
|
+
vec4 t2 = texelFetch(uMaterialsTex, ivec2(i + 2, 1), 0); // e2 | emit.g
|
|
332
|
+
vec4 t3 = texelFetch(uMaterialsTex, ivec2(i + 3, 1), 0); // n | emit.b
|
|
333
|
+
|
|
334
|
+
vec2 u = rand2();
|
|
335
|
+
if (u.x + u.y > 1.0) u = 1.0 - u; // uniform over the triangle
|
|
336
|
+
vec3 lp = t0.xyz + t1.xyz * u.x + t2.xyz * u.y;
|
|
337
|
+
|
|
338
|
+
vec3 d = lp - P;
|
|
339
|
+
float d2 = dot(d, d);
|
|
340
|
+
float dist = sqrt(d2);
|
|
341
|
+
if (dist < 1e-4) return vec3(0.0);
|
|
342
|
+
vec3 wi = d / dist;
|
|
343
|
+
|
|
344
|
+
float cosS = dot(N, wi);
|
|
345
|
+
// abs(): double-sided emission, matching what a GI ray hitting either face sees.
|
|
346
|
+
float cosL = abs(dot(t3.xyz, wi));
|
|
347
|
+
if (cosS <= 0.0 || cosL < 1e-4) return vec3(0.0);
|
|
348
|
+
if (occluded(P + N * uEps, wi, dist)) return vec3(0.0);
|
|
349
|
+
|
|
350
|
+
// Pick of one tri (probability 1/invProb) + uniform point on it:
|
|
351
|
+
// pdf_area = 1/(invProb·area). Solid-angle conversion gives irradiance
|
|
352
|
+
// Le · cosS · cosL / (d² · pdf_area).
|
|
353
|
+
vec3 e = vec3(t1.w, t2.w, t3.w) * (cosS * cosL * invProb * t0.w / max(d2, 1e-6));
|
|
354
|
+
|
|
355
|
+
// Dielectric highlight from this emitter: e already folds in cosS, so the
|
|
356
|
+
// specular is e * (GGX BRDF) toward the sampled point (wi).
|
|
357
|
+
if (gWantSpec) gSpec += e * ggxSpec(N, wi);
|
|
358
|
+
|
|
359
|
+
// Uniform-area sampling has huge single-sample variance for receivers close
|
|
360
|
+
// to a big emitter (sampled point can land almost on top of P, d² → 0);
|
|
361
|
+
// those 100× spikes read as speckles because the EMA decays them only as
|
|
362
|
+
// 1/count. Clamp at 2× the indirect firefly limit — slight bias right next
|
|
363
|
+
// to the emitter, stable everywhere.
|
|
364
|
+
float eLum = dot(e, vec3(0.299, 0.587, 0.114));
|
|
365
|
+
float eCap = uFireflyClamp * 2.0;
|
|
366
|
+
if (eLum > eCap) e *= eCap / eLum;
|
|
367
|
+
return e;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Shade this pixel's ReSTIR reservoir winner: recompute the (unshadowed)
|
|
371
|
+
// contribution — MUST match RestirPass.candidateContribution — then pay the
|
|
372
|
+
// one visibility ray and weight by W = wSum / (M · p̂). Analytic lights
|
|
373
|
+
// re-draw their soft-radius jitter here (the reservoir stores which light,
|
|
374
|
+
// not the jitter). The estimator inherently tames near-emitter spikes: a huge
|
|
375
|
+
// contribution comes with a proportionally huge p̂, and W divides it out.
|
|
376
|
+
vec3 shadeReservoir(vec3 P, vec3 N) {
|
|
377
|
+
// Spatial-stage encoding: r = id, a = precomputed W (vs. centroid score).
|
|
378
|
+
vec4 res = texture(uReservoir, vUv);
|
|
379
|
+
if (res.a <= 0.0) return vec3(0.0);
|
|
380
|
+
float id = res.r;
|
|
381
|
+
|
|
382
|
+
vec3 C;
|
|
383
|
+
vec3 wi;
|
|
384
|
+
float maxDist;
|
|
385
|
+
if (id < float(MAX_LIGHTS)) {
|
|
386
|
+
int i = int(id);
|
|
387
|
+
vec4 posType = uLightPosType[i];
|
|
388
|
+
vec4 colRad = uLightColorRadius[i];
|
|
389
|
+
if (posType.w < 0.5 || posType.w >= 1.5) {
|
|
390
|
+
vec3 d = posType.xyz - P;
|
|
391
|
+
float dl = length(d);
|
|
392
|
+
if (dl < 1e-5) return vec3(0.0);
|
|
393
|
+
float NdotL = dot(N, d / dl);
|
|
394
|
+
if (NdotL <= 0.0) return vec3(0.0);
|
|
395
|
+
float cone = spotFalloff(i, -d / dl);
|
|
396
|
+
if (cone <= 0.0) return vec3(0.0);
|
|
397
|
+
C = colRad.rgb * (cone * NdotL / (dl * dl));
|
|
398
|
+
vec3 lp = posType.xyz + randUnitVector() * colRad.w; // soft shadows
|
|
399
|
+
vec3 dj = lp - P;
|
|
400
|
+
maxDist = length(dj);
|
|
401
|
+
if (maxDist < 1e-5) return vec3(0.0);
|
|
402
|
+
wi = dj / maxDist;
|
|
403
|
+
} else {
|
|
404
|
+
float NdotL = dot(N, -posType.xyz);
|
|
405
|
+
if (NdotL <= 0.0) return vec3(0.0);
|
|
406
|
+
C = colRad.rgb * NdotL;
|
|
407
|
+
wi = normalize(-posType.xyz + randUnitVector() * colRad.w);
|
|
408
|
+
maxDist = 1e7;
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
int t = (int(id) - MAX_LIGHTS) * 4;
|
|
412
|
+
vec4 t0 = texelFetch(uMaterialsTex, ivec2(t, 1), 0);
|
|
413
|
+
vec4 t1 = texelFetch(uMaterialsTex, ivec2(t + 1, 1), 0);
|
|
414
|
+
vec4 t2 = texelFetch(uMaterialsTex, ivec2(t + 2, 1), 0);
|
|
415
|
+
vec4 t3 = texelFetch(uMaterialsTex, ivec2(t + 3, 1), 0);
|
|
416
|
+
// v3: the reservoir chose the TRIANGLE; draw a FRESH point on it every
|
|
417
|
+
// frame so the area light keeps averaging (no frozen-point noise). W was
|
|
418
|
+
// normalized against the centroid score, and E[point sample] = the
|
|
419
|
+
// triangle's true contribution, so the estimator stays consistent.
|
|
420
|
+
vec2 uv = rand2();
|
|
421
|
+
if (uv.x + uv.y > 1.0) uv = 1.0 - uv;
|
|
422
|
+
vec3 lp = t0.xyz + t1.xyz * uv.x + t2.xyz * uv.y;
|
|
423
|
+
vec3 d = lp - P;
|
|
424
|
+
float d2 = dot(d, d);
|
|
425
|
+
maxDist = sqrt(d2);
|
|
426
|
+
if (maxDist < 1e-4) return vec3(0.0);
|
|
427
|
+
wi = d / maxDist;
|
|
428
|
+
float cosS = dot(N, wi);
|
|
429
|
+
float cosL = abs(dot(t3.xyz, wi));
|
|
430
|
+
if (cosS <= 0.0 || cosL < 1e-4) return vec3(0.0);
|
|
431
|
+
C = vec3(t1.w, t2.w, t3.w) * (cosS * cosL * t0.w / max(d2, 1e-6));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (occluded(P + N * uEps, wi, maxDist)) return vec3(0.0);
|
|
435
|
+
// Dielectric highlight from the reservoir winner (C = li * cos, shared with
|
|
436
|
+
// the diffuse term; W = res.a is applied to both).
|
|
437
|
+
if (gWantSpec) gSpec += C * (ggxSpec(N, wi) * res.a);
|
|
438
|
+
vec3 e = C * res.a;
|
|
439
|
+
// Safety clamp, same budget as the emissive direct clamp elsewhere.
|
|
440
|
+
float l = dot(e, vec3(0.299, 0.587, 0.114));
|
|
441
|
+
float cap = uFireflyClamp * 2.0;
|
|
442
|
+
if (l > cap) e *= cap / l;
|
|
443
|
+
return e;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ONE light sample for secondary path vertices: stochastically pick either the
|
|
447
|
+
// analytic lights or the emissive set (weighted 1/p). Costs a single shadow
|
|
448
|
+
// ray — same ray budget the GI bounce had before emissive NEE existed —
|
|
449
|
+
// instead of two; the estimator stays unbiased and temporal accumulation
|
|
450
|
+
// averages out the extra variance.
|
|
451
|
+
vec3 sampleOneAny(vec3 P, vec3 N) {
|
|
452
|
+
bool hasL = uLightCount > 0;
|
|
453
|
+
bool hasE = uEmissiveCount > 0;
|
|
454
|
+
if (hasL && hasE) {
|
|
455
|
+
return rand() < 0.5
|
|
456
|
+
? sampleOneLight(P, N) * 2.0
|
|
457
|
+
: sampleEmissiveTri(P, N) * 2.0;
|
|
458
|
+
}
|
|
459
|
+
if (hasL) return sampleOneLight(P, N);
|
|
460
|
+
if (hasE) return sampleEmissiveTri(P, N);
|
|
461
|
+
return vec3(0.0);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Incoming radiance along rd: trace, shade the hit with direct + NEE lighting,
|
|
465
|
+
// sky/env on a miss. Specular rays keep emitter emission on hit (NEE at the ray
|
|
466
|
+
// origin cannot cover a specular path); diffuse GI rays drop it for NEE-listed
|
|
467
|
+
// (static) emitters so that light isn't counted twice.
|
|
468
|
+
vec3 traceRadiance(vec3 ro, vec3 rd, bool specular) {
|
|
469
|
+
uvec4 fi; vec3 bary; float dist; bool isDyn;
|
|
470
|
+
if (!traceBoth(ro, rd, fi, bary, dist, isDyn)) {
|
|
471
|
+
return uSkyEnabled
|
|
472
|
+
? skyColor(rd, uSunDir, uSunColor, uSkyZenith, uSkyHorizon, uSkyIntensity)
|
|
473
|
+
: uEnvColor * uEnvIntensity;
|
|
474
|
+
}
|
|
475
|
+
vec4 attr = isDyn
|
|
476
|
+
? textureSampleBarycoord(uAttrDynamic, bary, fi.xyz)
|
|
477
|
+
: textureSampleBarycoord(uAttrStatic, bary, fi.xyz);
|
|
478
|
+
vec3 hAlbedo; float hRough; vec3 hEmissive; float hMetal;
|
|
479
|
+
fetchMaterial(attr.w, hAlbedo, hRough, hEmissive, hMetal);
|
|
480
|
+
vec3 hN = normalize(attr.xyz);
|
|
481
|
+
if (dot(hN, rd) > 0.0) hN = -hN;
|
|
482
|
+
vec3 hP = ro + rd * dist;
|
|
483
|
+
vec3 Ld = sampleOneAny(hP + hN * uEps, hN);
|
|
484
|
+
vec3 hLe = (!specular && uEmissiveCount > 0 && !isDyn) ? vec3(0.0) : hEmissive;
|
|
485
|
+
return hLe + hAlbedo * Ld * (1.0 / PI);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
float schlick(float cosT, float eta) {
|
|
489
|
+
float r0 = (1.0 - eta) / (1.0 + eta);
|
|
490
|
+
r0 *= r0;
|
|
491
|
+
return r0 + (1.0 - r0) * pow(1.0 - cosT, 5.0);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Roughness-jittered mirror direction (glossy cone approximation).
|
|
495
|
+
vec3 glossyReflect(vec3 V, vec3 N, float rough) {
|
|
496
|
+
vec3 refl = reflect(V, N);
|
|
497
|
+
if (rough > 0.0) {
|
|
498
|
+
refl = normalize(mix(refl, cosineSampleHemisphere(N, rand2()), rough * rough));
|
|
499
|
+
}
|
|
500
|
+
return refl;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Analytic lights live in uniform arrays, not the BVH, so a traced reflection
|
|
504
|
+
// ray never sees them — a mirror under a spotlight would show no glint. Evaluate
|
|
505
|
+
// each light as a small area source along the (roughness-jittered) reflection
|
|
506
|
+
// direction: if refl points within the light's angular radius, the light's disc
|
|
507
|
+
// is reflected, so add its radiance. The jitter in refl (from glossyReflect)
|
|
508
|
+
// softens the disc over temporal accumulation, widening the glint with
|
|
509
|
+
// roughness. Shadowed with the same any-hit occluder as direct lighting.
|
|
510
|
+
vec3 analyticGlint(vec3 P, vec3 refl) {
|
|
511
|
+
vec3 sum = vec3(0.0);
|
|
512
|
+
for (int i = 0; i < MAX_LIGHTS; i++) {
|
|
513
|
+
if (i >= uLightCount) break;
|
|
514
|
+
vec4 posType = uLightPosType[i];
|
|
515
|
+
vec4 colRad = uLightColorRadius[i];
|
|
516
|
+
if (posType.w < 0.5 || posType.w >= 1.5) {
|
|
517
|
+
// point / spot
|
|
518
|
+
vec3 d = posType.xyz - P;
|
|
519
|
+
float dl = length(d);
|
|
520
|
+
if (dl < 1e-4) continue;
|
|
521
|
+
vec3 toL = d / dl;
|
|
522
|
+
float cone = spotFalloff(i, -toL);
|
|
523
|
+
if (cone <= 0.0) continue;
|
|
524
|
+
// Angular radius of the sphere light + a small floor so a zero-radius
|
|
525
|
+
// light still shows a pin-point glint.
|
|
526
|
+
float ang = atan(max(colRad.w, 1e-3) / dl) + 0.01;
|
|
527
|
+
if (dot(refl, toL) < cos(ang)) continue;
|
|
528
|
+
if (occluded(P + refl * uEps, refl, dl)) continue;
|
|
529
|
+
sum += colRad.rgb * (cone / (dl * dl));
|
|
530
|
+
} else {
|
|
531
|
+
// directional: fixed small angular size (colRad.w = sun softness)
|
|
532
|
+
vec3 toL = normalize(-posType.xyz);
|
|
533
|
+
float ang = max(colRad.w, 0.02) + 0.01;
|
|
534
|
+
if (dot(refl, toL) < cos(ang)) continue;
|
|
535
|
+
if (occluded(P + refl * uEps, refl, 1e7)) continue;
|
|
536
|
+
sum += colRad.rgb;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return sum;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Glass: Fresnel-weighted blend of a surface reflection and a two-interface
|
|
543
|
+
// refraction (enter at P, march to the exit surface, refract again).
|
|
544
|
+
vec3 glassRadiance(vec3 P, vec3 N, vec3 V, float rough) {
|
|
545
|
+
vec3 refl = glossyReflect(V, N, rough);
|
|
546
|
+
vec3 reflRad = dot(refl, N) > 0.0
|
|
547
|
+
? traceRadiance(P + N * uEps, refl, true) + analyticGlint(P, refl)
|
|
548
|
+
: vec3(0.0);
|
|
549
|
+
|
|
550
|
+
float eta = 1.0 / uIor;
|
|
551
|
+
vec3 rd = refract(V, N, eta);
|
|
552
|
+
if (rd == vec3(0.0)) return reflRad; // total internal reflection at entry
|
|
553
|
+
float fres = schlick(clamp(-dot(V, N), 0.0, 1.0), eta);
|
|
554
|
+
|
|
555
|
+
vec3 ro = P - N * (2.0 * uEps);
|
|
556
|
+
vec3 refrRad;
|
|
557
|
+
uvec4 fi; vec3 bary; float dist; bool isDyn;
|
|
558
|
+
if (traceBoth(ro, rd, fi, bary, dist, isDyn)) {
|
|
559
|
+
// Exit interface: refract back out (or bounce once on internal reflection).
|
|
560
|
+
vec4 attr = isDyn
|
|
561
|
+
? textureSampleBarycoord(uAttrDynamic, bary, fi.xyz)
|
|
562
|
+
: textureSampleBarycoord(uAttrStatic, bary, fi.xyz);
|
|
563
|
+
vec3 xN = normalize(attr.xyz);
|
|
564
|
+
if (dot(xN, rd) > 0.0) xN = -xN;
|
|
565
|
+
vec3 xP = ro + rd * dist;
|
|
566
|
+
vec3 rd2 = refract(rd, xN, uIor);
|
|
567
|
+
if (rd2 == vec3(0.0)) rd2 = reflect(rd, xN);
|
|
568
|
+
refrRad = traceRadiance(xP - xN * uEps, rd2, true);
|
|
569
|
+
} else {
|
|
570
|
+
refrRad = uSkyEnabled
|
|
571
|
+
? skyColor(rd, uSunDir, uSunColor, uSkyZenith, uSkyHorizon, uSkyIntensity)
|
|
572
|
+
: uEnvColor * uEnvIntensity;
|
|
573
|
+
}
|
|
574
|
+
return mix(refrRad, reflRad, fres);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
void main() {
|
|
578
|
+
vec4 wp = texture(uGWorldPos, vUv);
|
|
579
|
+
if (wp.w < 0.5) {
|
|
580
|
+
outIrradiance = vec4(0.0);
|
|
581
|
+
outSpecular = vec4(0.0);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
ivec2 px = ivec2(gl_FragCoord.xy);
|
|
586
|
+
gSeed = uint(px.x) * 1973u + uint(px.y) * 9277u + uint(uFrame) * 26699u;
|
|
587
|
+
gSeed = pcgHash(gSeed);
|
|
588
|
+
gBlueNoise = fetchBlueNoise();
|
|
589
|
+
gBnDim = 0;
|
|
590
|
+
|
|
591
|
+
vec3 P = wp.xyz;
|
|
592
|
+
vec4 nmSample = texture(uGNormalMetal, vUv);
|
|
593
|
+
vec3 N = normalize(nmSample.xyz);
|
|
594
|
+
// Decode the packed material word (see GBufferPass): [4,5] → alpha blend
|
|
595
|
+
// (w - 4 = opacity), [2,4) → glass (w - 2 = transmission), else metalness.
|
|
596
|
+
float matW = nmSample.w;
|
|
597
|
+
bool blend = matW >= 4.0;
|
|
598
|
+
float opacity = blend ? clamp(matW - 4.0, 0.0, 1.0) : 1.0;
|
|
599
|
+
float transmission = (matW >= 2.0 && matW < 4.0) ? clamp(matW - 2.0, 0.0, 1.0) : 0.0;
|
|
600
|
+
float metal = matW < 2.0 ? matW : 0.0;
|
|
601
|
+
float rough = clamp(wp.w - 1.0, 0.0, 1.0);
|
|
602
|
+
|
|
603
|
+
// Cook-Torrance specular state for this primary surface. gWantSpec gates the
|
|
604
|
+
// GGX term to PRIMARY direct lighting only (GI-bounce direct light, below,
|
|
605
|
+
// reuses the same functions but must not pollute the highlight buffer).
|
|
606
|
+
gSpec = vec3(0.0);
|
|
607
|
+
gViewDir = normalize(uCameraPos - P);
|
|
608
|
+
gSpecRough = rough;
|
|
609
|
+
gWantSpec = true;
|
|
610
|
+
|
|
611
|
+
// --- direct lighting ---
|
|
612
|
+
// ReSTIR: shade the reservoir's winner with one visibility ray (flat cost in
|
|
613
|
+
// light count). Stochastic: one blind random sample. Full: one shadow ray
|
|
614
|
+
// per light + one for the emissive set.
|
|
615
|
+
vec3 direct = vec3(0.0);
|
|
616
|
+
if (uRestirEnabled) {
|
|
617
|
+
direct = shadeReservoir(P, N);
|
|
618
|
+
} else if (uLightStochastic) {
|
|
619
|
+
direct = sampleOneAny(P, N);
|
|
620
|
+
} else {
|
|
621
|
+
for (int i = 0; i < MAX_LIGHTS; i++) {
|
|
622
|
+
if (i >= uLightCount) break;
|
|
623
|
+
direct += lightContribution(i, P, N);
|
|
624
|
+
}
|
|
625
|
+
// Emissive meshes as area lights (next-event estimation, one shadow ray).
|
|
626
|
+
direct += sampleEmissiveTri(P, N);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// --- 1-bounce indirect (cosine-weighted; pdf cancels the NdotL/PI).
|
|
630
|
+
// traceRadiance shades the hit with direct + NEE light, or returns the
|
|
631
|
+
// sky/env colour when the ray escapes (the natural ambient bounce).
|
|
632
|
+
// Half-rate mode traces on alternating checkerboard parity each frame,
|
|
633
|
+
// DOUBLED — the temporal average converges to the same brightness
|
|
634
|
+
// (unbiased) while GI's ray cost halves; accumulation + denoise absorb
|
|
635
|
+
// the alternation.
|
|
636
|
+
gWantSpec = false; // secondary bounces contribute to diffuse GI only
|
|
637
|
+
vec3 indirect = vec3(0.0);
|
|
638
|
+
if (uGIEnabled) {
|
|
639
|
+
bool trace = !uGIHalfRate || (((px.x + px.y + int(uFrame)) & 1) == 0);
|
|
640
|
+
if (trace) {
|
|
641
|
+
indirect = traceRadiance(P + N * uEps, cosineSampleHemisphere(N, rand2()), false);
|
|
642
|
+
if (uGIHalfRate) indirect *= 2.0;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Firefly clamp: suppress rare huge GI samples (big perceived-noise win,
|
|
647
|
+
// slightly biased). Applied to indirect only; direct is analytic.
|
|
648
|
+
float lum = dot(indirect, vec3(0.299, 0.587, 0.114));
|
|
649
|
+
if (lum > uFireflyClamp) indirect *= uFireflyClamp / lum;
|
|
650
|
+
|
|
651
|
+
vec3 sampleIrr = direct + indirect;
|
|
652
|
+
|
|
653
|
+
// --- traced specular: mirror/glossy reflections on metals ---
|
|
654
|
+
if (uReflEnabled && metal > 0.001) {
|
|
655
|
+
vec3 V = normalize(P - uCameraPos);
|
|
656
|
+
vec3 refl = glossyReflect(V, N, rough);
|
|
657
|
+
if (dot(refl, N) > 0.0) {
|
|
658
|
+
// Metals have no diffuse term: replace by metalness. The composite's
|
|
659
|
+
// albedo multiply then tints the reflection (F0 = albedo for metals).
|
|
660
|
+
// analyticGlint adds the direct lights the reflection ray cannot see, so
|
|
661
|
+
// a metal under a spotlight shows a proper (albedo-tinted) glint.
|
|
662
|
+
vec3 reflRad = traceRadiance(P + N * uEps, refl, true) + analyticGlint(P, refl);
|
|
663
|
+
sampleIrr = mix(sampleIrr, reflRad, metal);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// --- traced glass: Fresnel reflection + two-interface refraction ---
|
|
668
|
+
if (uRefrEnabled && transmission > 0.001) {
|
|
669
|
+
vec3 V = normalize(P - uCameraPos);
|
|
670
|
+
sampleIrr = mix(sampleIrr, glassRadiance(P, N, V, rough), transmission);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// --- alpha blend: straight-through view continuation ---
|
|
674
|
+
// A transparent surface is primary-visible in the G-buffer but was kept out of
|
|
675
|
+
// the BVH, so a ray along the view direction passes THROUGH it to whatever is
|
|
676
|
+
// behind. Trace that continuation and shade it like a glass/GI hit (emitters
|
|
677
|
+
// keep their emission — this is direct visibility through the pane — sky/env on
|
|
678
|
+
// a miss). The two quantities live at DIFFERENT scales: sampleIrr is the
|
|
679
|
+
// pane's own demodulated surface light (composite re-applies albedo), while
|
|
680
|
+
// the behind trace is final outgoing radiance — mixing them in one slot makes
|
|
681
|
+
// the pane term drown out what shows through. So the behind image rides the
|
|
682
|
+
// SPECULAR attachment instead (composite adds that buffer without the albedo
|
|
683
|
+
// multiply, and its short-history accumulation suits behind-content that
|
|
684
|
+
// parallaxes against the pane), and CompositePass performs the opacity blend
|
|
685
|
+
// where the pane's albedo is actually available. sampleIrr keeps only the
|
|
686
|
+
// pane's own surface lighting, which is static on the surface and accumulates
|
|
687
|
+
// with normal full-length history.
|
|
688
|
+
vec3 blendBehind = vec3(0.0);
|
|
689
|
+
if (uBlendEnabled && blend) {
|
|
690
|
+
vec3 V = normalize(P - uCameraPos);
|
|
691
|
+
blendBehind = traceRadiance(P + V * uEps, V, true);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// A single NaN/Inf sample would poison the EMA history for good (mix() with
|
|
695
|
+
// NaN stays NaN until a disocclusion resets the pixel) — sanitize first.
|
|
696
|
+
if (any(isnan(sampleIrr)) || any(isinf(sampleIrr))) sampleIrr = vec3(0.0);
|
|
697
|
+
|
|
698
|
+
// Fresh dielectric direct specular for this frame. Metals/glass carry their
|
|
699
|
+
// (albedo-tinted) specular in the reflection path above, so scale their share
|
|
700
|
+
// out of the white buffer — the effective F0 is mix(0.04, albedo, metal),
|
|
701
|
+
// split across the two buffers. The separate SpecularAccumPass reprojects and
|
|
702
|
+
// temporally accumulates this with a short (near-mirror) history.
|
|
703
|
+
// Blend pixels repurpose this attachment for the straight-through behind
|
|
704
|
+
// radiance (see above) — their dielectric highlight is dropped, a fair trade
|
|
705
|
+
// for a correct-scale see-through image.
|
|
706
|
+
vec3 spec = blend ? blendBehind : gSpec * ((1.0 - metal) * (1.0 - transmission));
|
|
707
|
+
if (any(isnan(spec)) || any(isinf(spec))) spec = vec3(0.0);
|
|
708
|
+
if (!blend) {
|
|
709
|
+
float specLum = dot(spec, vec3(0.299, 0.587, 0.114));
|
|
710
|
+
float specCap = uFireflyClamp * 4.0; // narrow lobes spike; keep the EMA stable
|
|
711
|
+
if (specLum > specCap) spec *= specCap / specLum;
|
|
712
|
+
}
|
|
713
|
+
outSpecular = vec4(spec, 1.0);
|
|
714
|
+
|
|
715
|
+
// --- temporal reprojection: pull validated history from last frame ---
|
|
716
|
+
float count = 1.0;
|
|
717
|
+
vec3 history = vec3(0.0);
|
|
718
|
+
if (uTemporalReprojection) {
|
|
719
|
+
vec4 clip = uPrevViewProj * vec4(P, 1.0);
|
|
720
|
+
vec4 clipC = uViewProj * vec4(P, 1.0);
|
|
721
|
+
if (clip.w > 0.0 && clipC.w > 0.0) {
|
|
722
|
+
vec2 prevUv = (clip.xy / clip.w) * 0.5 + 0.5;
|
|
723
|
+
// P comes from a full-res G-buffer texel, which sits sub-pixel off this
|
|
724
|
+
// half-res fragment's center. That constant offset would bias bilinear
|
|
725
|
+
// history reads every frame (content drifts/smears at renderScale < 1).
|
|
726
|
+
// Cancel it: measure P's offset in the CURRENT frame and subtract.
|
|
727
|
+
vec2 currUv = (clipC.xy / clipC.w) * 0.5 + 0.5;
|
|
728
|
+
prevUv -= currUv - vUv;
|
|
729
|
+
if (prevUv.x >= 0.0 && prevUv.x <= 1.0 && prevUv.y >= 0.0 && prevUv.y <= 1.0) {
|
|
730
|
+
vec4 prevPos = texture(uPrevGWorldPos, prevUv);
|
|
731
|
+
// Plane-distance test: robust at grazing angles (position error from
|
|
732
|
+
// texel quantization lies along the surface, not along the normal).
|
|
733
|
+
float distToCam = distance(P, uCameraPos);
|
|
734
|
+
float tol = 0.005 * distToCam + 20.0 * uEps;
|
|
735
|
+
bool valid = prevPos.w > 0.5
|
|
736
|
+
&& abs(dot(P - prevPos.xyz, N)) < tol;
|
|
737
|
+
if (valid) {
|
|
738
|
+
vec4 h = texture(uPrevAccum, prevUv); // bilinear
|
|
739
|
+
// Mirror-like pixels keep a SHORT history: their reflected content
|
|
740
|
+
// moves differently from the surface, so long history smears the
|
|
741
|
+
// reflection under camera motion — and specular rays are nearly
|
|
742
|
+
// deterministic, so they don't need the accumulation anyway.
|
|
743
|
+
float specHist = max(metal, transmission) * (1.0 - rough);
|
|
744
|
+
// (Blend pixels need no shortening here: this slot holds only the
|
|
745
|
+
// pane's own surface light, which is static on the surface. The
|
|
746
|
+
// parallaxing behind-image rides the specular attachment, whose
|
|
747
|
+
// accumulation is short-history by design.)
|
|
748
|
+
float histCap = mix(uMaxHistory, min(uMaxHistory, 10.0), specHist);
|
|
749
|
+
count = clamp(h.a, 0.0, histCap) + 1.0;
|
|
750
|
+
history = h.rgb;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Exponential moving average; count=1 (disocclusion / first frame) means
|
|
757
|
+
// the fresh sample is used as-is.
|
|
758
|
+
vec3 blended = mix(history, sampleIrr, 1.0 / count);
|
|
759
|
+
outIrradiance = vec4(blended, count);
|
|
760
|
+
}
|
|
761
|
+
`;
|
|
762
|
+
|
|
763
|
+
// Specular accumulation: the lighting pass emits FRESH dielectric specular in
|
|
764
|
+
// MRT attachment 1 (it has no spare sampler to read its own specular history).
|
|
765
|
+
// This second, cheap program reprojects that fresh sample against the previous
|
|
766
|
+
// accumulated specular and EMA-blends it — the same temporal scheme as the
|
|
767
|
+
// irradiance buffer, but with the SHORT (near-mirror) history a view-dependent
|
|
768
|
+
// highlight needs so it tracks moving lights and the camera without smearing.
|
|
769
|
+
const specAccumFrag = /* glsl */ `
|
|
770
|
+
precision highp float;
|
|
771
|
+
|
|
772
|
+
layout(location = 0) out vec4 outSpec;
|
|
773
|
+
|
|
774
|
+
in vec2 vUv;
|
|
775
|
+
|
|
776
|
+
uniform sampler2D uFreshSpec;
|
|
777
|
+
uniform sampler2D uPrevSpec;
|
|
778
|
+
uniform sampler2D uGWorldPos;
|
|
779
|
+
uniform sampler2D uGNormalMetal;
|
|
780
|
+
uniform sampler2D uPrevGWorldPos;
|
|
781
|
+
uniform mat4 uPrevViewProj;
|
|
782
|
+
uniform mat4 uViewProj;
|
|
783
|
+
uniform vec3 uCameraPos;
|
|
784
|
+
uniform float uEps;
|
|
785
|
+
uniform float uMaxHistory;
|
|
786
|
+
uniform bool uTemporalReprojection;
|
|
787
|
+
|
|
788
|
+
void main() {
|
|
789
|
+
vec4 wp = texture(uGWorldPos, vUv);
|
|
790
|
+
if (wp.w < 0.5) { outSpec = vec4(0.0); return; }
|
|
791
|
+
vec3 P = wp.xyz;
|
|
792
|
+
vec3 N = normalize(texture(uGNormalMetal, vUv).xyz);
|
|
793
|
+
float rough = clamp(wp.w - 1.0, 0.0, 1.0);
|
|
794
|
+
vec3 fresh = texture(uFreshSpec, vUv).rgb;
|
|
795
|
+
|
|
796
|
+
float count = 1.0;
|
|
797
|
+
vec3 history = vec3(0.0);
|
|
798
|
+
if (uTemporalReprojection) {
|
|
799
|
+
vec4 clip = uPrevViewProj * vec4(P, 1.0);
|
|
800
|
+
vec4 clipC = uViewProj * vec4(P, 1.0);
|
|
801
|
+
if (clip.w > 0.0 && clipC.w > 0.0) {
|
|
802
|
+
vec2 prevUv = (clip.xy / clip.w) * 0.5 + 0.5;
|
|
803
|
+
vec2 currUv = (clipC.xy / clipC.w) * 0.5 + 0.5;
|
|
804
|
+
prevUv -= currUv - vUv; // cancel the G-buffer texel sub-pixel offset
|
|
805
|
+
if (prevUv.x >= 0.0 && prevUv.x <= 1.0 && prevUv.y >= 0.0 && prevUv.y <= 1.0) {
|
|
806
|
+
vec4 prevPos = texture(uPrevGWorldPos, prevUv);
|
|
807
|
+
float tol = 0.005 * distance(P, uCameraPos) + 20.0 * uEps;
|
|
808
|
+
if (prevPos.w > 0.5 && abs(dot(P - prevPos.xyz, N)) < tol) {
|
|
809
|
+
vec4 h = texture(uPrevSpec, prevUv);
|
|
810
|
+
// Short history: specular is view-dependent, so a long EMA smears the
|
|
811
|
+
// highlight under motion. Smoother (sharper) highlights react fastest.
|
|
812
|
+
float specHist = 1.0 - rough;
|
|
813
|
+
float histCap = mix(min(uMaxHistory, 32.0), min(uMaxHistory, 8.0), specHist);
|
|
814
|
+
count = clamp(h.a, 0.0, histCap) + 1.0;
|
|
815
|
+
history = h.rgb;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
vec3 blended = mix(history, fresh, 1.0 / count);
|
|
822
|
+
if (any(isnan(blended)) || any(isinf(blended))) blended = vec3(0.0);
|
|
823
|
+
outSpec = vec4(blended, count);
|
|
824
|
+
}
|
|
825
|
+
`;
|
|
826
|
+
|
|
827
|
+
// Irradiance-history carry for renderScale/canvas resizes. The shared CopyPass
|
|
828
|
+
// writes ONE output; rendering it into the 2-attachment MRT is a draw-buffer
|
|
829
|
+
// mismatch that some drivers (ANGLE/D3D11) reject with INVALID_OPERATION. This
|
|
830
|
+
// 2-output copy matches the MRT: attachment 0 = resampled history (alpha/count
|
|
831
|
+
// clamped), attachment 1 = 0 (fresh-written next frame anyway).
|
|
832
|
+
const mrtCarryFrag = /* glsl */ `
|
|
833
|
+
precision highp float;
|
|
834
|
+
layout(location = 0) out vec4 o0;
|
|
835
|
+
layout(location = 1) out vec4 o1;
|
|
836
|
+
in vec2 vUv;
|
|
837
|
+
uniform sampler2D uTex;
|
|
838
|
+
uniform float uCountClamp;
|
|
839
|
+
void main() {
|
|
840
|
+
vec4 c = texture(uTex, vUv);
|
|
841
|
+
if (uCountClamp >= 0.0) c.a = min(c.a, uCountClamp);
|
|
842
|
+
o0 = c;
|
|
843
|
+
o1 = vec4(0.0);
|
|
844
|
+
}
|
|
845
|
+
`;
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Fullscreen pass: for every G-buffer pixel, trace shadow rays to every light and
|
|
849
|
+
* one cosine-weighted GI bounce against the BVH. Outputs demodulated irradiance,
|
|
850
|
+
* progressively accumulated into a ping-pong float target while the camera is still.
|
|
851
|
+
*
|
|
852
|
+
* The target is a 2-attachment MRT: [0] demodulated diffuse irradiance,
|
|
853
|
+
* [1] FRESH dielectric direct specular (temporally accumulated by specAccumFrag
|
|
854
|
+
* into a second ping-pong pair, specA/specB).
|
|
855
|
+
*/
|
|
856
|
+
export class RTLightingPass {
|
|
857
|
+
constructor(width, height) {
|
|
858
|
+
this.targetA = this._makeTarget(width, height);
|
|
859
|
+
this.targetB = this._makeTarget(width, height);
|
|
860
|
+
// Accumulated specular history (ping-pong), fed by the fresh specular in
|
|
861
|
+
// targetA/B attachment 1.
|
|
862
|
+
this.specA = this._makeSpecTarget(width, height);
|
|
863
|
+
this.specB = this._makeSpecTarget(width, height);
|
|
864
|
+
|
|
865
|
+
this.material = new THREE.ShaderMaterial({
|
|
866
|
+
glslVersion: THREE.GLSL3,
|
|
867
|
+
vertexShader: fullscreenVert,
|
|
868
|
+
fragmentShader: rtLightingFrag,
|
|
869
|
+
uniforms: {
|
|
870
|
+
bvhStatic: { value: null },
|
|
871
|
+
bvhDynamic: { value: null },
|
|
872
|
+
uHasDynamic: { value: false },
|
|
873
|
+
uAttrStatic: { value: null },
|
|
874
|
+
uAttrDynamic: { value: null },
|
|
875
|
+
uMaterialsTex: { value: null },
|
|
876
|
+
uGWorldPos: { value: null },
|
|
877
|
+
uGNormalMetal: { value: null },
|
|
878
|
+
uPrevAccum: { value: null },
|
|
879
|
+
uPrevGWorldPos: { value: null },
|
|
880
|
+
uReservoir: { value: null },
|
|
881
|
+
uRestirEnabled: { value: false },
|
|
882
|
+
uPrevViewProj: { value: new THREE.Matrix4() },
|
|
883
|
+
uViewProj: { value: new THREE.Matrix4() },
|
|
884
|
+
uCameraPos: { value: new THREE.Vector3() },
|
|
885
|
+
uMaxHistory: { value: 128 },
|
|
886
|
+
uTemporalReprojection: { value: true },
|
|
887
|
+
uFireflyClamp: { value: 4.0 },
|
|
888
|
+
uLightPosType: { value: [] },
|
|
889
|
+
uLightColorRadius: { value: [] },
|
|
890
|
+
uLightDirCone: { value: [] },
|
|
891
|
+
uLightCount: { value: 0 },
|
|
892
|
+
uEmissiveCount: { value: 0 },
|
|
893
|
+
uEmissiveCDF: { value: true },
|
|
894
|
+
uReflEnabled: { value: true },
|
|
895
|
+
uRefrEnabled: { value: true },
|
|
896
|
+
uBlendEnabled: { value: true },
|
|
897
|
+
uIor: { value: 1.5 },
|
|
898
|
+
uLightStochastic: { value: false },
|
|
899
|
+
uGIHalfRate: { value: false },
|
|
900
|
+
uEnvColor: { value: new THREE.Color(0.03, 0.04, 0.06) },
|
|
901
|
+
uEnvIntensity: { value: 1.0 },
|
|
902
|
+
uFrame: { value: 0 },
|
|
903
|
+
uEps: { value: 1e-3 },
|
|
904
|
+
uGIEnabled: { value: true },
|
|
905
|
+
uSkyEnabled: { value: false },
|
|
906
|
+
uSunDir: { value: new THREE.Vector3(0.4, 0.8, 0.45).normalize() },
|
|
907
|
+
uSunColor: { value: new THREE.Color(1.0, 0.9, 0.75) },
|
|
908
|
+
uSkyZenith: { value: new THREE.Color(0.18, 0.34, 0.62) },
|
|
909
|
+
uSkyHorizon: { value: new THREE.Color(0.7, 0.8, 0.9) },
|
|
910
|
+
uSkyIntensity: { value: 1.0 },
|
|
911
|
+
},
|
|
912
|
+
depthTest: false,
|
|
913
|
+
depthWrite: false,
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
// Specular temporal accumulation program (its own sampler budget — well
|
|
917
|
+
// clear of the lighting pass's 16-sampler ceiling).
|
|
918
|
+
this.specMaterial = new THREE.ShaderMaterial({
|
|
919
|
+
glslVersion: THREE.GLSL3,
|
|
920
|
+
vertexShader: fullscreenVert,
|
|
921
|
+
fragmentShader: specAccumFrag,
|
|
922
|
+
uniforms: {
|
|
923
|
+
uFreshSpec: { value: null },
|
|
924
|
+
uPrevSpec: { value: null },
|
|
925
|
+
uGWorldPos: { value: null },
|
|
926
|
+
uGNormalMetal: { value: null },
|
|
927
|
+
uPrevGWorldPos: { value: null },
|
|
928
|
+
uPrevViewProj: { value: new THREE.Matrix4() },
|
|
929
|
+
uViewProj: { value: new THREE.Matrix4() },
|
|
930
|
+
uCameraPos: { value: new THREE.Vector3() },
|
|
931
|
+
uEps: { value: 1e-3 },
|
|
932
|
+
uMaxHistory: { value: 128 },
|
|
933
|
+
uTemporalReprojection: { value: true },
|
|
934
|
+
},
|
|
935
|
+
depthTest: false,
|
|
936
|
+
depthWrite: false,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// 2-output history carry (see mrtCarryFrag) — matches the MRT's draw buffers.
|
|
940
|
+
this.carryMaterial = new THREE.ShaderMaterial({
|
|
941
|
+
glslVersion: THREE.GLSL3,
|
|
942
|
+
vertexShader: fullscreenVert,
|
|
943
|
+
fragmentShader: mrtCarryFrag,
|
|
944
|
+
uniforms: { uTex: { value: null }, uCountClamp: { value: -1 } },
|
|
945
|
+
depthTest: false,
|
|
946
|
+
depthWrite: false,
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
this.scene = new THREE.Scene();
|
|
950
|
+
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
951
|
+
this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
|
|
952
|
+
this.quad.frustumCulled = false;
|
|
953
|
+
this.scene.add(this.quad);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
_makeTarget(width, height) {
|
|
957
|
+
// Half-float + linear: history is sampled bilinearly at reprojected UVs,
|
|
958
|
+
// and fp16 halves the bandwidth (EMA blending never accumulates a raw sum,
|
|
959
|
+
// so fp16 precision is sufficient). Two attachments: [0] irradiance,
|
|
960
|
+
// [1] fresh dielectric specular.
|
|
961
|
+
const t = new THREE.WebGLMultipleRenderTargets(width, height, 2, {
|
|
962
|
+
minFilter: THREE.LinearFilter,
|
|
963
|
+
magFilter: THREE.LinearFilter,
|
|
964
|
+
format: THREE.RGBAFormat,
|
|
965
|
+
type: THREE.HalfFloatType,
|
|
966
|
+
depthBuffer: false,
|
|
967
|
+
stencilBuffer: false,
|
|
968
|
+
});
|
|
969
|
+
for (const tex of t.texture) tex.generateMipmaps = false;
|
|
970
|
+
return t;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
_makeSpecTarget(width, height) {
|
|
974
|
+
const t = new THREE.WebGLRenderTarget(width, height, {
|
|
975
|
+
minFilter: THREE.LinearFilter,
|
|
976
|
+
magFilter: THREE.LinearFilter,
|
|
977
|
+
format: THREE.RGBAFormat,
|
|
978
|
+
type: THREE.HalfFloatType,
|
|
979
|
+
depthBuffer: false,
|
|
980
|
+
stencilBuffer: false,
|
|
981
|
+
});
|
|
982
|
+
t.texture.generateMipmaps = false;
|
|
983
|
+
return t;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** Clear both history buffers (e.g. after scene recompile or resize). */
|
|
987
|
+
clearHistory(renderer) {
|
|
988
|
+
const prevTarget = renderer.getRenderTarget();
|
|
989
|
+
const prevColor = new THREE.Color();
|
|
990
|
+
renderer.getClearColor(prevColor);
|
|
991
|
+
const prevAlpha = renderer.getClearAlpha();
|
|
992
|
+
renderer.setClearColor(0x000000, 0);
|
|
993
|
+
for (const t of [this.targetA, this.targetB, this.specA, this.specB]) {
|
|
994
|
+
renderer.setRenderTarget(t);
|
|
995
|
+
renderer.clear(true, false, false);
|
|
996
|
+
}
|
|
997
|
+
renderer.setRenderTarget(prevTarget);
|
|
998
|
+
renderer.setClearColor(prevColor, prevAlpha);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
setSize(width, height) {
|
|
1002
|
+
this.targetA.setSize(width, height);
|
|
1003
|
+
this.targetB.setSize(width, height);
|
|
1004
|
+
this.specA.setSize(width, height);
|
|
1005
|
+
this.specB.setSize(width, height);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Reallocate the history targets to a new size while PRESERVING the
|
|
1010
|
+
* accumulated irradiance. The plain setSize + clearHistory path dumps every
|
|
1011
|
+
* temporal sample, which strobes the image back to 1-spp noise on every
|
|
1012
|
+
* governor renderScale step — this carries the history over instead.
|
|
1013
|
+
*
|
|
1014
|
+
* The freshest history is targetB (last frame's output — see the swap in
|
|
1015
|
+
* render()); it is resampled through copyPass into the new targetB, its
|
|
1016
|
+
* per-pixel sample count (alpha) clamped to `carryFrames` so the EMA
|
|
1017
|
+
* reconverges smoothly at the new resolution rather than freezing on stale
|
|
1018
|
+
* values. targetA is overwritten on the next render, so it only needs the
|
|
1019
|
+
* fresh allocation, not a copy.
|
|
1020
|
+
*/
|
|
1021
|
+
resizeCarry(renderer, copyPass, width, height, carryFrames) {
|
|
1022
|
+
const newA = this._makeTarget(width, height);
|
|
1023
|
+
const newB = this._makeTarget(width, height);
|
|
1024
|
+
// Carry the irradiance history (attachment 0) with the 2-output carry
|
|
1025
|
+
// material so the draw matches the MRT's draw buffers (a 1-output CopyPass
|
|
1026
|
+
// blit here is INVALID_OPERATION on ANGLE/D3D11). Attachment 1 is fresh-
|
|
1027
|
+
// written every frame, so it needs no carry.
|
|
1028
|
+
this.carryMaterial.uniforms.uTex.value = this.targetB.texture[0];
|
|
1029
|
+
this.carryMaterial.uniforms.uCountClamp.value = carryFrames;
|
|
1030
|
+
this.quad.material = this.carryMaterial;
|
|
1031
|
+
const prev = renderer.getRenderTarget();
|
|
1032
|
+
renderer.setRenderTarget(newB);
|
|
1033
|
+
renderer.render(this.scene, this.camera);
|
|
1034
|
+
renderer.setRenderTarget(prev);
|
|
1035
|
+
this.quad.material = this.material;
|
|
1036
|
+
this.targetA.dispose();
|
|
1037
|
+
this.targetB.dispose();
|
|
1038
|
+
this.targetA = newA;
|
|
1039
|
+
this.targetB = newB;
|
|
1040
|
+
|
|
1041
|
+
// Specular history carries the same way (freshest is specB — see render()).
|
|
1042
|
+
const newSpecA = this._makeSpecTarget(width, height);
|
|
1043
|
+
const newSpecB = this._makeSpecTarget(width, height);
|
|
1044
|
+
copyPass.blit(renderer, this.specB.texture, newSpecB, carryFrames);
|
|
1045
|
+
this.specA.dispose();
|
|
1046
|
+
this.specB.dispose();
|
|
1047
|
+
this.specA = newSpecA;
|
|
1048
|
+
this.specB = newSpecB;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
setCompiledScene(compiled) {
|
|
1052
|
+
const u = this.material.uniforms;
|
|
1053
|
+
u.bvhStatic.value = compiled.staticBvhUniform;
|
|
1054
|
+
u.bvhDynamic.value = compiled.dynamicBvhUniform;
|
|
1055
|
+
u.uHasDynamic.value = compiled.hasDynamic;
|
|
1056
|
+
u.uAttrStatic.value = compiled.staticAttrTex;
|
|
1057
|
+
u.uAttrDynamic.value = compiled.dynamicAttrTex;
|
|
1058
|
+
u.uMaterialsTex.value = compiled.materialsTex;
|
|
1059
|
+
u.uLightPosType.value = compiled.lightPosType;
|
|
1060
|
+
u.uLightColorRadius.value = compiled.lightColorRadius;
|
|
1061
|
+
u.uLightDirCone.value = compiled.lightDirCone;
|
|
1062
|
+
u.uLightCount.value = compiled.lightCount;
|
|
1063
|
+
u.uEmissiveCount.value = compiled.emissiveTriCount;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Renders lighting into targetA (reading targetB as irradiance history), then
|
|
1068
|
+
* accumulates the fresh specular (targetA attachment 1) into specA (reading
|
|
1069
|
+
* specB as history). Swaps both ping-pong pairs. Returns { irradiance,
|
|
1070
|
+
* specular } textures for this frame.
|
|
1071
|
+
*/
|
|
1072
|
+
render(renderer, gbuffer, frame, reservoirTexture = null) {
|
|
1073
|
+
const u = this.material.uniforms;
|
|
1074
|
+
u.uGWorldPos.value = gbuffer.worldPos;
|
|
1075
|
+
u.uGNormalMetal.value = gbuffer.normalMetal;
|
|
1076
|
+
u.uPrevGWorldPos.value = gbuffer.prevWorldPos;
|
|
1077
|
+
u.uPrevAccum.value = this.targetB.texture[0];
|
|
1078
|
+
u.uReservoir.value = reservoirTexture;
|
|
1079
|
+
u.uRestirEnabled.value = reservoirTexture !== null;
|
|
1080
|
+
u.uFrame.value = frame;
|
|
1081
|
+
|
|
1082
|
+
// 1. lighting (MRT): [0] accumulated irradiance, [1] fresh specular.
|
|
1083
|
+
this.quad.material = this.material;
|
|
1084
|
+
renderer.setRenderTarget(this.targetA);
|
|
1085
|
+
renderer.render(this.scene, this.camera);
|
|
1086
|
+
|
|
1087
|
+
// 2. specular temporal accumulation: fresh (targetA[1]) + history (specB).
|
|
1088
|
+
const su = this.specMaterial.uniforms;
|
|
1089
|
+
su.uFreshSpec.value = this.targetA.texture[1];
|
|
1090
|
+
su.uPrevSpec.value = this.specB.texture;
|
|
1091
|
+
su.uGWorldPos.value = gbuffer.worldPos;
|
|
1092
|
+
su.uGNormalMetal.value = gbuffer.normalMetal;
|
|
1093
|
+
su.uPrevGWorldPos.value = gbuffer.prevWorldPos;
|
|
1094
|
+
su.uPrevViewProj.value.copy(u.uPrevViewProj.value);
|
|
1095
|
+
su.uViewProj.value.copy(u.uViewProj.value);
|
|
1096
|
+
su.uCameraPos.value.copy(u.uCameraPos.value);
|
|
1097
|
+
su.uEps.value = u.uEps.value;
|
|
1098
|
+
su.uMaxHistory.value = u.uMaxHistory.value;
|
|
1099
|
+
su.uTemporalReprojection.value = u.uTemporalReprojection.value;
|
|
1100
|
+
this.quad.material = this.specMaterial;
|
|
1101
|
+
renderer.setRenderTarget(this.specA);
|
|
1102
|
+
renderer.render(this.scene, this.camera);
|
|
1103
|
+
|
|
1104
|
+
this.quad.material = this.material; // restore for the next caller
|
|
1105
|
+
renderer.setRenderTarget(null);
|
|
1106
|
+
|
|
1107
|
+
const outIrr = this.targetA.texture[0];
|
|
1108
|
+
const outSpec = this.specA.texture;
|
|
1109
|
+
[this.targetA, this.targetB] = [this.targetB, this.targetA];
|
|
1110
|
+
[this.specA, this.specB] = [this.specB, this.specA];
|
|
1111
|
+
return { irradiance: outIrr, specular: outSpec };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
dispose() {
|
|
1115
|
+
this.targetA.dispose();
|
|
1116
|
+
this.targetB.dispose();
|
|
1117
|
+
this.specA.dispose();
|
|
1118
|
+
this.specB.dispose();
|
|
1119
|
+
this.material.dispose();
|
|
1120
|
+
this.specMaterial.dispose();
|
|
1121
|
+
this.carryMaterial.dispose();
|
|
1122
|
+
this.quad.geometry.dispose();
|
|
1123
|
+
}
|
|
1124
|
+
}
|