rayzee 7.4.1 → 7.5.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/README.md +1 -1
- package/dist/rayzee.es.js +1754 -1383
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +145 -53
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +4 -0
- package/src/Processor/AssetLoader.js +7 -1
- package/src/Processor/KernelManager.js +1 -2
- package/src/Processor/LightSerializer.js +56 -12
- package/src/Processor/QueueManager.js +37 -1
- package/src/Processor/blackbody.js +74 -0
- package/src/Stages/PathTracer.js +75 -2
- package/src/Stages/PathTracerStage.js +2 -2
- package/src/TSL/LightsCore.js +139 -12
- package/src/TSL/LightsSampling.js +210 -17
- package/src/TSL/SortGlobalKernels.js +135 -0
- package/src/TSL/patches.js +90 -0
- package/src/managers/LightManager.js +26 -4
- package/src/managers/UniformManager.js +1 -1
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -88,6 +88,10 @@ export const ENGINE_DEFAULTS = {
|
|
|
88
88
|
performanceModeAdaptive: 'medium',
|
|
89
89
|
|
|
90
90
|
fireflyThreshold: 3.0,
|
|
91
|
+
// Wavefront material-coherence sort: global counting-sort of entering rays by material before
|
|
92
|
+
// Shade (material-pure workgroups), under dynamic dispatch. Measured −8% at 1024²/8b. Gated on
|
|
93
|
+
// material count > 8; the histogram bin count is sized per-scene to the material count.
|
|
94
|
+
wavefrontSortMaterials: true,
|
|
91
95
|
renderLimitMode: 'frames',
|
|
92
96
|
renderTimeLimit: 30,
|
|
93
97
|
renderMode: 0,
|
|
@@ -1446,12 +1446,18 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1446
1446
|
|
|
1447
1447
|
if ( userData.type === 'RectAreaLight' ) {
|
|
1448
1448
|
|
|
1449
|
+
// Intensity is authored in Watts (Blender-style); emitted radiance
|
|
1450
|
+
// is computed at render time as power/(π·area). Blender emission
|
|
1451
|
+
// defaults: power-normalized, full Lambertian spread (π), rectangle.
|
|
1449
1452
|
const light = new RectAreaLight(
|
|
1450
1453
|
new Color( ...userData.color ),
|
|
1451
|
-
userData.intensity * 0.1
|
|
1454
|
+
userData.intensity * 0.1,
|
|
1452
1455
|
userData.width,
|
|
1453
1456
|
userData.height
|
|
1454
1457
|
);
|
|
1458
|
+
light.userData.normalize = userData.normalize ?? true;
|
|
1459
|
+
light.userData.spread = Number.isFinite( userData.spread ) ? userData.spread : Math.PI;
|
|
1460
|
+
light.userData.shape = userData.shape ?? 'rectangle';
|
|
1455
1461
|
light.position.z = - 2;
|
|
1456
1462
|
light.name = userData.name;
|
|
1457
1463
|
object.add( light );
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
const WORKGROUP_SIZES = {
|
|
13
13
|
generate: [ 16, 16, 1 ], // 2D screen-space
|
|
14
14
|
extend: [ 256, 1, 1 ], // 1D ray-parallel
|
|
15
|
-
|
|
16
|
-
shade: [ 256, 1, 1 ], // 1D ray-parallel (sorted)
|
|
15
|
+
shade: [ 256, 1, 1 ], // 1D ray-parallel (material-sorted when sort enabled)
|
|
17
16
|
connect: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
18
17
|
accumulate: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
19
18
|
compact: [ 256, 1, 1 ], // 1D ray-parallel
|
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
import { Vector3, Quaternion } from 'three';
|
|
2
|
+
import { blackbodyToLinearRGB } from './blackbody.js';
|
|
3
|
+
|
|
4
|
+
// Point & spot lights specify Power in Watts; the shader uses radiant intensity
|
|
5
|
+
// (W/sr) as `intensity / dist²`, so convert with I = P / 4π (isotropic emitter),
|
|
6
|
+
// matching Blender. Area lights are already radiant power; the Sun is irradiance.
|
|
7
|
+
const INV_4PI = 1 / ( 4 * Math.PI );
|
|
8
|
+
|
|
9
|
+
// Effective emission colour + intensity after per-light Exposure (EV stops) and
|
|
10
|
+
// optional blackbody Temperature tint (Blender-style). Both fold into the values
|
|
11
|
+
// already sent to the GPU — no shader or struct change needed.
|
|
12
|
+
function effectiveEmission( light ) {
|
|
13
|
+
|
|
14
|
+
const ud = light.userData || {};
|
|
15
|
+
const exposure = Number.isFinite( ud.exposure ) ? ud.exposure : 0;
|
|
16
|
+
const intensity = light.intensity * Math.pow( 2, exposure );
|
|
17
|
+
|
|
18
|
+
let r = light.color.r, g = light.color.g, b = light.color.b;
|
|
19
|
+
if ( ud.useTemperature ) {
|
|
20
|
+
|
|
21
|
+
const [ tr, tg, tb ] = blackbodyToLinearRGB( ud.temperature ?? 6500 );
|
|
22
|
+
r *= tr; g *= tg; b *= tb;
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { r, g, b, intensity };
|
|
27
|
+
|
|
28
|
+
}
|
|
2
29
|
|
|
3
30
|
export class LightSerializer {
|
|
4
31
|
|
|
@@ -90,6 +117,8 @@ export class LightSerializer {
|
|
|
90
117
|
// Get angle parameter from light (default to 0 for sharp shadows)
|
|
91
118
|
const angle = light.userData.angle || light.angle || 0.0; // In radians
|
|
92
119
|
|
|
120
|
+
const em = effectiveEmission( light );
|
|
121
|
+
|
|
93
122
|
// Optional projection mask. Sign of intensity carries the inverted flag.
|
|
94
123
|
const gobo = light.userData?.gobo;
|
|
95
124
|
const goboIndex = ( gobo && Number.isInteger( gobo.index ) ) ? gobo.index : - 1;
|
|
@@ -101,8 +130,8 @@ export class LightSerializer {
|
|
|
101
130
|
this.directionalLightCache.push( {
|
|
102
131
|
data: [
|
|
103
132
|
direction.x, direction.y, direction.z, // direction toward light (3)
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
134
|
+
em.intensity, // intensity (1) — incl. exposure
|
|
106
135
|
angle, // angular diameter in radians (1)
|
|
107
136
|
goboIndex, // gobo layer index, -1 = none (1)
|
|
108
137
|
goboIntensity, // signed gobo strength (1)
|
|
@@ -133,14 +162,25 @@ export class LightSerializer {
|
|
|
133
162
|
// Calculate importance for sorting
|
|
134
163
|
const importance = this.calculateLightImportance( light, 'area' );
|
|
135
164
|
|
|
136
|
-
//
|
|
165
|
+
// Blender-style emission controls (stored on userData; sensible defaults).
|
|
166
|
+
// normalize ON → radiance ∝ 1/area (resizing keeps total power constant).
|
|
167
|
+
const normalize = ( light.userData?.normalize ?? true ) ? 1.0 : 0.0;
|
|
168
|
+
const spread = Number.isFinite( light.userData?.spread ) ? light.userData.spread : Math.PI;
|
|
169
|
+
const shape = ( light.userData?.shape === 'ellipse' || light.userData?.shape === 'disk' || light.userData?.shape === 1 ) ? 1.0 : 0.0;
|
|
170
|
+
|
|
171
|
+
const em = effectiveEmission( light );
|
|
172
|
+
|
|
173
|
+
// Store in cache with importance (16 floats, vec4-aligned)
|
|
137
174
|
this.areaLightCache.push( {
|
|
138
175
|
data: [
|
|
139
176
|
position.x, position.y, position.z, // position (3)
|
|
140
|
-
u.x, u.y, u.z, // u vector (3)
|
|
141
|
-
v.x, v.y, v.z, // v vector (3)
|
|
142
|
-
|
|
143
|
-
|
|
177
|
+
u.x, u.y, u.z, // u half-vector (3)
|
|
178
|
+
v.x, v.y, v.z, // v half-vector (3)
|
|
179
|
+
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
180
|
+
em.intensity, // radiant power in Watts (1) — incl. exposure
|
|
181
|
+
normalize, // power-normalize flag (1)
|
|
182
|
+
spread, // emission spread in radians (1)
|
|
183
|
+
shape, // 0 = rect, 1 = disk/ellipse (1)
|
|
144
184
|
],
|
|
145
185
|
importance: importance,
|
|
146
186
|
light: light
|
|
@@ -159,12 +199,14 @@ export class LightSerializer {
|
|
|
159
199
|
// Calculate importance for sorting
|
|
160
200
|
const importance = this.calculateLightImportance( light, 'point' );
|
|
161
201
|
|
|
202
|
+
const em = effectiveEmission( light );
|
|
203
|
+
|
|
162
204
|
// Store in cache with importance
|
|
163
205
|
this.pointLightCache.push( {
|
|
164
206
|
data: [
|
|
165
207
|
position.x, position.y, position.z, // position (3)
|
|
166
|
-
|
|
167
|
-
|
|
208
|
+
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
209
|
+
em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
|
|
168
210
|
light.distance || 0.0, // cutoff distance (0 = infinite) (1)
|
|
169
211
|
light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
|
|
170
212
|
],
|
|
@@ -198,13 +240,15 @@ export class LightSerializer {
|
|
|
198
240
|
const iesIndex = ( ies && Number.isInteger( ies.index ) ) ? ies.index : - 1;
|
|
199
241
|
const iesIntensity = ( ies && typeof ies.intensity === 'number' ) ? ies.intensity : 1.0;
|
|
200
242
|
|
|
243
|
+
const em = effectiveEmission( light );
|
|
244
|
+
|
|
201
245
|
// Store in cache with importance
|
|
202
246
|
this.spotLightCache.push( {
|
|
203
247
|
data: [
|
|
204
248
|
position.x, position.y, position.z, // position (3)
|
|
205
249
|
direction.x, direction.y, direction.z, // direction (3)
|
|
206
|
-
|
|
207
|
-
|
|
250
|
+
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
251
|
+
em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
|
|
208
252
|
light.angle || Math.PI / 4, // cone half-angle in radians (1)
|
|
209
253
|
light.penumbra || 0.0, // penumbra [0,1] (1)
|
|
210
254
|
light.distance || 0.0, // cutoff distance (0 = infinite) (1)
|
|
@@ -290,7 +334,7 @@ export class LightSerializer {
|
|
|
290
334
|
|
|
291
335
|
// Divide flat array lengths by per-light stride to get actual light counts
|
|
292
336
|
const directionalCount = Math.floor( this.lightData.directional.length / 12 );
|
|
293
|
-
const areaCount = Math.floor( this.lightData.rectArea.length /
|
|
337
|
+
const areaCount = Math.floor( this.lightData.rectArea.length / 16 );
|
|
294
338
|
const pointCount = Math.floor( this.lightData.point.length / 9 );
|
|
295
339
|
const spotCount = Math.floor( this.lightData.spot.length / 20 );
|
|
296
340
|
|
|
@@ -40,6 +40,9 @@ export class QueueManager {
|
|
|
40
40
|
// A/B alternate: one read by current bounce, other written by compaction
|
|
41
41
|
this.activeIndices = null;
|
|
42
42
|
this.activeIndicesRO = null;
|
|
43
|
+
this.sortedIndices = null;
|
|
44
|
+
this.sortedIndicesRO = null;
|
|
45
|
+
this.sortGlobalHistogram = null;
|
|
43
46
|
this.pingPong = 0; // 0 = read A / write B, 1 = read B / write A
|
|
44
47
|
|
|
45
48
|
if ( maxRays > 0 ) {
|
|
@@ -83,11 +86,23 @@ export class QueueManager {
|
|
|
83
86
|
b: storage( attrB, 'uint' ).toReadOnly(),
|
|
84
87
|
};
|
|
85
88
|
|
|
89
|
+
// Material-sort output: a material-reordered permutation of the active index
|
|
90
|
+
// list, written by the global material sort and read by Shade in place of activeIndices.
|
|
91
|
+
const sortAttr = new StorageInstancedBufferAttribute( new Uint32Array( capacity ), 1 );
|
|
92
|
+
this._sortAttr = sortAttr;
|
|
93
|
+
this.sortedIndices = storage( sortAttr, 'uint' );
|
|
94
|
+
this.sortedIndicesRO = storage( sortAttr, 'uint' ).toReadOnly();
|
|
95
|
+
|
|
96
|
+
// Global material-sort histogram, sized to the bin cap (SORT_GLOBAL_MAX_BINS=256); kernels use
|
|
97
|
+
// only the first `bins` entries (= per-scene material count). 256 × 4B = 1KB.
|
|
98
|
+
this._sortGlobalHistAttr = new StorageInstancedBufferAttribute( new Uint32Array( 256 ), 1 );
|
|
99
|
+
this.sortGlobalHistogram = storage( this._sortGlobalHistAttr, 'uint' ).toAtomic();
|
|
100
|
+
|
|
86
101
|
this.pingPong = 0;
|
|
87
102
|
|
|
88
103
|
const totalBytes = (
|
|
89
104
|
COUNTER.COUNT * 4 +
|
|
90
|
-
capacity * 4 *
|
|
105
|
+
capacity * 4 * 3
|
|
91
106
|
);
|
|
92
107
|
|
|
93
108
|
console.log(
|
|
@@ -131,6 +146,24 @@ export class QueueManager {
|
|
|
131
146
|
|
|
132
147
|
}
|
|
133
148
|
|
|
149
|
+
getSortedRW() {
|
|
150
|
+
|
|
151
|
+
return this.sortedIndices;
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getSortedRO() {
|
|
156
|
+
|
|
157
|
+
return this.sortedIndicesRO;
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getSortGlobalHistogram() {
|
|
162
|
+
|
|
163
|
+
return this.sortGlobalHistogram;
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
|
|
134
167
|
// raw attribute for `renderer.getArrayBufferAsync(...)` readback
|
|
135
168
|
getCountersAttribute() {
|
|
136
169
|
|
|
@@ -167,6 +200,9 @@ export class QueueManager {
|
|
|
167
200
|
this.counters = null;
|
|
168
201
|
this.activeIndices = null;
|
|
169
202
|
this.activeIndicesRO = null;
|
|
203
|
+
this.sortedIndices = null;
|
|
204
|
+
this.sortedIndicesRO = null;
|
|
205
|
+
this.sortGlobalHistogram = null;
|
|
170
206
|
this.capacity = 0;
|
|
171
207
|
|
|
172
208
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Blackbody temperature → linear Rec.709 (= linear sRGB) RGB.
|
|
2
|
+
//
|
|
3
|
+
// Ported verbatim from Blender/Cycles `svm_math_blackbody_color_rec709`
|
|
4
|
+
// (Lukas Stockner's 7-segment rational/cubic fit; coefficients from
|
|
5
|
+
// intern/cycles/kernel/tables.h). The fit is luminance-balanced — the Rec.709
|
|
6
|
+
// luminance of the returned colour is ≈1.0 across the whole range — so it can be
|
|
7
|
+
// multiplied straight into a light's colour without altering its power: the
|
|
8
|
+
// temperature shifts hue only, brightness stays governed by intensity. This is
|
|
9
|
+
// exactly how Cycles applies it (raw multiply into the light colour, no
|
|
10
|
+
// renormalisation).
|
|
11
|
+
|
|
12
|
+
const BB_R = [
|
|
13
|
+
[ 1.61919106e3, - 2.05010916e-3, 5.02995757e0 ],
|
|
14
|
+
[ 2.48845471e3, - 1.11330907e-3, 3.22621544e0 ],
|
|
15
|
+
[ 3.34143193e3, - 4.86551192e-4, 1.76486769e0 ],
|
|
16
|
+
[ 4.09461742e3, - 1.27446582e-4, 7.25731635e-1 ],
|
|
17
|
+
[ 4.67028036e3, 2.91258199e-5, 1.26703442e-1 ],
|
|
18
|
+
[ 4.59509185e3, 2.87495649e-5, 1.50345020e-1 ],
|
|
19
|
+
[ 3.78717450e3, 9.35907826e-6, 3.99075871e-1 ],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const BB_G = [
|
|
23
|
+
[ - 4.88999748e2, 6.04330754e-4, - 7.55807526e-2 ],
|
|
24
|
+
[ - 7.55994277e2, 3.16730098e-4, 4.78306139e-1 ],
|
|
25
|
+
[ - 1.02363977e3, 1.20223470e-4, 9.36662319e-1 ],
|
|
26
|
+
[ - 1.26571316e3, 4.87340896e-6, 1.27054498e0 ],
|
|
27
|
+
[ - 1.42529332e3, - 4.01150431e-5, 1.43972784e0 ],
|
|
28
|
+
[ - 1.17554822e3, - 2.16378048e-5, 1.30408023e0 ],
|
|
29
|
+
[ - 5.00799571e2, - 4.59832026e-6, 1.09098763e0 ],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const BB_B = [
|
|
33
|
+
[ 5.96945309e-11, - 4.85742887e-8, - 9.70622247e-5, - 4.07936148e-3 ],
|
|
34
|
+
[ 2.40430366e-11, 5.55021075e-8, - 1.98503712e-4, 2.89312858e-2 ],
|
|
35
|
+
[ - 1.40949732e-11, 1.89878968e-7, - 3.56632824e-4, 9.10767778e-2 ],
|
|
36
|
+
[ - 3.61460868e-11, 2.84822009e-7, - 4.93211319e-4, 1.56723440e-1 ],
|
|
37
|
+
[ - 1.97075738e-11, 1.75359352e-7, - 2.50542825e-4, - 2.22783266e-2 ],
|
|
38
|
+
[ - 1.61997957e-13, - 1.64216008e-8, 3.86216271e-4, - 7.38077418e-1 ],
|
|
39
|
+
[ 6.72650283e-13, - 2.73078809e-8, 4.24098264e-4, - 7.52335691e-1 ],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert a colour temperature in Kelvin to linear Rec.709 / linear-sRGB RGB.
|
|
44
|
+
* Negative out-of-gamut channels (very low temperatures) are clamped to 0.
|
|
45
|
+
* @param {number} kelvin
|
|
46
|
+
* @returns {[number, number, number]}
|
|
47
|
+
*/
|
|
48
|
+
export function blackbodyToLinearRGB( kelvin ) {
|
|
49
|
+
|
|
50
|
+
let r, g, b;
|
|
51
|
+
|
|
52
|
+
if ( kelvin >= 12000 ) {
|
|
53
|
+
|
|
54
|
+
r = 0.8262954810464208; g = 0.9945080501520986; b = 1.566307710274283;
|
|
55
|
+
|
|
56
|
+
} else if ( kelvin < 800 ) {
|
|
57
|
+
|
|
58
|
+
r = 5.413294490189271; g = - 0.20319390035873933; b = - 0.0822535242887164;
|
|
59
|
+
|
|
60
|
+
} else {
|
|
61
|
+
|
|
62
|
+
const i = kelvin >= 6365 ? 6 : kelvin >= 3315 ? 5 : kelvin >= 1902 ? 4
|
|
63
|
+
: kelvin >= 1449 ? 3 : kelvin >= 1167 ? 2 : kelvin >= 965 ? 1 : 0;
|
|
64
|
+
const cr = BB_R[ i ], cg = BB_G[ i ], cb = BB_B[ i ];
|
|
65
|
+
const tInv = 1 / kelvin;
|
|
66
|
+
r = cr[ 0 ] * tInv + cr[ 1 ] * kelvin + cr[ 2 ];
|
|
67
|
+
g = cg[ 0 ] * tInv + cg[ 1 ] * kelvin + cg[ 2 ];
|
|
68
|
+
b = ( ( cb[ 0 ] * kelvin + cb[ 1 ] ) * kelvin + cb[ 2 ] ) * kelvin + cb[ 3 ];
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [ Math.max( 0, r ), Math.max( 0, g ), Math.max( 0, b ) ];
|
|
73
|
+
|
|
74
|
+
}
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -17,6 +17,11 @@ import { buildShadeKernel, SHADE_WG_SIZE } from '../TSL/ShadeKernel.js';
|
|
|
17
17
|
import { buildCompactKernel, buildCompactSubgroupKernel, COMPACT_WG_SIZE } from '../TSL/CompactKernel.js';
|
|
18
18
|
import { buildFinalWriteKernel, FINALWRITE_WG_SIZE } from '../TSL/FinalWriteKernel.js';
|
|
19
19
|
import { buildDebugKernel, DEBUG_WG_SIZE } from '../TSL/DebugKernel.js';
|
|
20
|
+
import {
|
|
21
|
+
buildResetGlobalHistKernel, buildGlobalHistKernel, buildGlobalPrefixKernel, buildGlobalScatterKernel,
|
|
22
|
+
SORT_GLOBAL_WG_SIZE, SORT_GLOBAL_MAX_BINS,
|
|
23
|
+
} from '../TSL/SortGlobalKernels.js';
|
|
24
|
+
import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
|
|
20
25
|
import {
|
|
21
26
|
Fn, uint, atomicStore, atomicLoad, instanceIndex, If, Return,
|
|
22
27
|
} from 'three/tsl';
|
|
@@ -43,6 +48,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
43
48
|
// CPU sizes per-bounce kernels from last frame's survivor curve; kernels bound on ENTERING_COUNT so over-sizing is safe. (indirect dispatch not viable — three.js doesn't sync compute-written indirect buffers across submissions)
|
|
44
49
|
this._useDynamicDispatch = true;
|
|
45
50
|
|
|
51
|
+
// Global material-coherence sort: set per-build from ENGINE_DEFAULTS + material count (>8).
|
|
52
|
+
// Reorders entering rays into material-pure workgroups before Shade; runs under dynamic dispatch
|
|
53
|
+
// (compact reads the unsorted active list, so the survivor set is unchanged). Measured −8% at 1024²/8b.
|
|
54
|
+
this._sortMaterials = false;
|
|
55
|
+
|
|
46
56
|
// Flag-gated off: perf-neutral vs atomic-append and adds a 'subgroups' feature dependency.
|
|
47
57
|
this._useSubgroupCompact = false;
|
|
48
58
|
|
|
@@ -90,7 +100,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
90
100
|
if ( ! qm ) return null;
|
|
91
101
|
return [
|
|
92
102
|
qm._countersAttr, qm._bounceCountsAttr,
|
|
93
|
-
qm._attrA, qm._attrB,
|
|
103
|
+
qm._attrA, qm._attrB, qm._sortAttr,
|
|
94
104
|
];
|
|
95
105
|
|
|
96
106
|
} );
|
|
@@ -250,6 +260,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
250
260
|
this._wfCurrentBounce.value = bounce;
|
|
251
261
|
|
|
252
262
|
// Functional-compaction path (dynamic dispatch): copyback keeps the read buffer dense, kernels sized to live survivors. Dynamic-off uses the full path (ENTERING=maxRays, identity buffer).
|
|
263
|
+
// Material sort is compatible: shade reads sortedIndices while compact still reads the UNSORTED active list (getActiveReadRO), so the survivor set is unchanged.
|
|
253
264
|
const useFunctionalCompaction = this._useDynamicDispatch;
|
|
254
265
|
if ( useFunctionalCompaction ) {
|
|
255
266
|
|
|
@@ -281,6 +292,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
281
292
|
const wg = [ Math.ceil( sized / 256 ), 1, 1 ];
|
|
282
293
|
km.setDispatchCount( 'extend', wg );
|
|
283
294
|
km.setDispatchCount( 'shade', wg );
|
|
295
|
+
km.setDispatchCount( 'globalHist', wg );
|
|
296
|
+
km.setDispatchCount( 'globalScatter', wg );
|
|
284
297
|
km.setDispatchCount( 'compact', wg );
|
|
285
298
|
km.setDispatchCount( 'compactCopyback', wg );
|
|
286
299
|
|
|
@@ -291,11 +304,23 @@ export class PathTracer extends PathTracerStage {
|
|
|
291
304
|
km.setDispatchCount( 'extend', full );
|
|
292
305
|
km.setDispatchCount( 'shade', full );
|
|
293
306
|
km.setDispatchCount( 'compact', full );
|
|
307
|
+
km.setDispatchCount( 'globalHist', full );
|
|
308
|
+
km.setDispatchCount( 'globalScatter', full );
|
|
294
309
|
|
|
295
310
|
}
|
|
296
311
|
|
|
297
312
|
// Extend/Shade kept separate (not fused): a fused kernel's register pressure drops occupancy more than fusion saves.
|
|
298
313
|
km.dispatch( 'extend' );
|
|
314
|
+
if ( this._sortMaterials ) {
|
|
315
|
+
|
|
316
|
+
// Global material counting sort (material-pure workgroups): reset, histogram, prefix-sum, scatter.
|
|
317
|
+
km.dispatch( 'resetGlobalHist' );
|
|
318
|
+
km.dispatch( 'globalHist' );
|
|
319
|
+
km.dispatch( 'globalPrefix' );
|
|
320
|
+
km.dispatch( 'globalScatter' );
|
|
321
|
+
|
|
322
|
+
}
|
|
323
|
+
|
|
299
324
|
km.dispatch( 'shade' );
|
|
300
325
|
|
|
301
326
|
km.dispatch( 'resetActiveCounter' );
|
|
@@ -660,6 +685,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
660
685
|
displacementMaps: freshDisplacementMaps,
|
|
661
686
|
};
|
|
662
687
|
|
|
688
|
+
// Material-coherence sort gate (experiment): only worthwhile above a few materials.
|
|
689
|
+
this._sortMaterials = ( ENGINE_DEFAULTS.wavefrontSortMaterials ?? false )
|
|
690
|
+
&& ( this.materialData?.materialCount ?? 0 ) > 8;
|
|
691
|
+
|
|
663
692
|
const extFn = buildExtendKernel( {
|
|
664
693
|
bvhBuffer: freshBvh,
|
|
665
694
|
triangleBuffer: freshTri,
|
|
@@ -677,6 +706,50 @@ export class PathTracer extends PathTracerStage {
|
|
|
677
706
|
)
|
|
678
707
|
);
|
|
679
708
|
|
|
709
|
+
// Material-coherence sort: reorder the entering-ray indices by material between
|
|
710
|
+
// Extend and Shade. Histogram is workgroup-shared (patches.js §4); Shade reads the output.
|
|
711
|
+
if ( this._sortMaterials ) {
|
|
712
|
+
|
|
713
|
+
const sgHist = qm.getSortGlobalHistogram();
|
|
714
|
+
const sortBins = Math.min( SORT_GLOBAL_MAX_BINS, this.materialData?.materialCount ?? SORT_GLOBAL_MAX_BINS );
|
|
715
|
+
this._kernelManager.register( 'resetGlobalHist',
|
|
716
|
+
buildResetGlobalHistKernel( { sortGlobalHistogram: sgHist, bins: sortBins } )().compute(
|
|
717
|
+
[ 1, 1, 1 ], [ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
this._kernelManager.register( 'globalHist',
|
|
721
|
+
buildGlobalHistKernel( {
|
|
722
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
723
|
+
activeIndicesReadRO: qm.getActiveReadRO(),
|
|
724
|
+
sortGlobalHistogram: sgHist,
|
|
725
|
+
counters,
|
|
726
|
+
bins: sortBins,
|
|
727
|
+
} )().compute(
|
|
728
|
+
[ Math.ceil( maxRays / SORT_GLOBAL_WG_SIZE ), 1, 1 ],
|
|
729
|
+
[ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
730
|
+
)
|
|
731
|
+
);
|
|
732
|
+
this._kernelManager.register( 'globalPrefix',
|
|
733
|
+
buildGlobalPrefixKernel( { sortGlobalHistogram: sgHist, bins: sortBins } )().compute(
|
|
734
|
+
[ 1, 1, 1 ], [ 1, 1, 1 ]
|
|
735
|
+
)
|
|
736
|
+
);
|
|
737
|
+
this._kernelManager.register( 'globalScatter',
|
|
738
|
+
buildGlobalScatterKernel( {
|
|
739
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
740
|
+
activeIndicesReadRO: qm.getActiveReadRO(),
|
|
741
|
+
sortedIndicesRW: qm.getSortedRW(),
|
|
742
|
+
sortGlobalHistogram: sgHist,
|
|
743
|
+
counters,
|
|
744
|
+
bins: sortBins,
|
|
745
|
+
} )().compute(
|
|
746
|
+
[ Math.ceil( maxRays / SORT_GLOBAL_WG_SIZE ), 1, 1 ],
|
|
747
|
+
[ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
748
|
+
)
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
}
|
|
752
|
+
|
|
680
753
|
const shadeFn = buildShadeKernel( {
|
|
681
754
|
gBufferRW,
|
|
682
755
|
envCompensationDelta: this.envCompensationDelta,
|
|
@@ -689,7 +762,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
689
762
|
rngBufferRW: pb.rngBuffer.rw,
|
|
690
763
|
hitBufferRO: pb.hitBuffer.ro,
|
|
691
764
|
counters,
|
|
692
|
-
activeIndicesRO: qm.getActiveReadRO(),
|
|
765
|
+
activeIndicesRO: this._sortMaterials ? qm.getSortedRO() : qm.getActiveReadRO(),
|
|
693
766
|
albedoMaps: freshAlbedoMaps,
|
|
694
767
|
normalMaps: freshNormalMaps,
|
|
695
768
|
bumpMaps: freshBumpMaps,
|
|
@@ -571,11 +571,11 @@ export class PathTracerStage extends RenderStage {
|
|
|
571
571
|
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
// Area lights (
|
|
574
|
+
// Area lights (16 floats per light — 13 base + normalize/spread/shape)
|
|
575
575
|
if ( this.areaLightsData && this.areaLightsData.length > 0 ) {
|
|
576
576
|
|
|
577
577
|
this.areaLightsBufferNode.array = Array.from( this.areaLightsData );
|
|
578
|
-
this.numAreaLights.value = Math.floor( this.areaLightsData.length /
|
|
578
|
+
this.numAreaLights.value = Math.floor( this.areaLightsData.length / 16 );
|
|
579
579
|
|
|
580
580
|
} else {
|
|
581
581
|
|
package/src/TSL/LightsCore.js
CHANGED
|
@@ -42,12 +42,15 @@ export const DirectionalLight = struct( {
|
|
|
42
42
|
|
|
43
43
|
export const AreaLight = struct( {
|
|
44
44
|
position: 'vec3',
|
|
45
|
-
u: 'vec3', // First axis of the rectangular light
|
|
46
|
-
v: 'vec3', // Second axis of the rectangular light
|
|
45
|
+
u: 'vec3', // First axis half-vector of the rectangular light
|
|
46
|
+
v: 'vec3', // Second axis half-vector of the rectangular light
|
|
47
47
|
color: 'vec3',
|
|
48
|
-
intensity: 'float',
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
intensity: 'float', // radiant power (Watts), Blender-style
|
|
49
|
+
normalize: 'float', // 1 = power normalized over area (radiance ∝ 1/area), 0 = constant radiance
|
|
50
|
+
spread: 'float', // emission cone half-fan in radians, π = full Lambertian hemisphere
|
|
51
|
+
shape: 'float', // 0 = rectangle, 1 = disk/ellipse
|
|
52
|
+
normal: 'vec3', // derived
|
|
53
|
+
area: 'float', // derived (true world area: rect = w·h, ellipse = π/4·w·h)
|
|
51
54
|
} );
|
|
52
55
|
|
|
53
56
|
export const PointLight = struct( {
|
|
@@ -125,7 +128,7 @@ export const getDirectionalLight = Fn( ( [ directionalLightsBuffer, index ] ) =>
|
|
|
125
128
|
|
|
126
129
|
export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
|
|
127
130
|
|
|
128
|
-
const baseIndex = index.mul(
|
|
131
|
+
const baseIndex = index.mul( 16 );
|
|
129
132
|
const u = vec3(
|
|
130
133
|
areaLightsBuffer.element( baseIndex.add( 3 ) ),
|
|
131
134
|
areaLightsBuffer.element( baseIndex.add( 4 ) ),
|
|
@@ -138,6 +141,10 @@ export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
|
|
|
138
141
|
).toVar();
|
|
139
142
|
|
|
140
143
|
const crossUV = cross( u, v );
|
|
144
|
+
const shape = areaLightsBuffer.element( baseIndex.add( 15 ) );
|
|
145
|
+
|
|
146
|
+
// u,v are half-vectors → full rectangle area = 4·|u×v|; disk/ellipse = π/4 of that.
|
|
147
|
+
const rectArea = length( crossUV ).mul( 4.0 );
|
|
141
148
|
|
|
142
149
|
return AreaLight( {
|
|
143
150
|
position: vec3(
|
|
@@ -153,8 +160,11 @@ export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
|
|
|
153
160
|
areaLightsBuffer.element( baseIndex.add( 11 ) ),
|
|
154
161
|
),
|
|
155
162
|
intensity: areaLightsBuffer.element( baseIndex.add( 12 ) ),
|
|
163
|
+
normalize: areaLightsBuffer.element( baseIndex.add( 13 ) ),
|
|
164
|
+
spread: areaLightsBuffer.element( baseIndex.add( 14 ) ),
|
|
165
|
+
shape: shape,
|
|
156
166
|
normal: normalize( crossUV ),
|
|
157
|
-
area:
|
|
167
|
+
area: select( shape.greaterThan( 0.5 ), rectArea.mul( 0.7853981633974483 ), rectArea ),
|
|
158
168
|
} );
|
|
159
169
|
|
|
160
170
|
} );
|
|
@@ -501,12 +511,18 @@ export const intersectAreaLight = Fn( ( [ light, rayOrigin, rayDirection ] ) =>
|
|
|
501
511
|
const u_dir = light.u.div( uLen );
|
|
502
512
|
const v_dir = light.v.div( vLen );
|
|
503
513
|
|
|
504
|
-
// Project onto axes
|
|
505
|
-
const u_proj = dot( localPoint, u_dir );
|
|
506
|
-
const v_proj = dot( localPoint, v_dir );
|
|
514
|
+
// Project onto axes, normalized to [-1,1] across the half-extents
|
|
515
|
+
const u_proj = dot( localPoint, u_dir ).div( uLen );
|
|
516
|
+
const v_proj = dot( localPoint, v_dir ).div( vLen );
|
|
507
517
|
|
|
508
|
-
//
|
|
509
|
-
|
|
518
|
+
// Rectangle: |u|≤1 ∧ |v|≤1. Disk/ellipse: u²+v²≤1.
|
|
519
|
+
const inside = select(
|
|
520
|
+
light.shape.greaterThan( 0.5 ),
|
|
521
|
+
u_proj.mul( u_proj ).add( v_proj.mul( v_proj ) ).lessThanEqual( 1.0 ),
|
|
522
|
+
abs( u_proj ).lessThanEqual( 1.0 ).and( abs( v_proj ).lessThanEqual( 1.0 ) ),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
If( inside, () => {
|
|
510
526
|
|
|
511
527
|
result.assign( t );
|
|
512
528
|
|
|
@@ -520,3 +536,114 @@ export const intersectAreaLight = Fn( ( [ light, rayOrigin, rayDirection ] ) =>
|
|
|
520
536
|
|
|
521
537
|
} );
|
|
522
538
|
|
|
539
|
+
// ================================================================================
|
|
540
|
+
// SPHERICAL RECTANGLE SAMPLING (Ureña et al. 2013, "An Area-Preserving
|
|
541
|
+
// Parametrization for Spherical Rectangles"). Same scheme Cycles uses for rect
|
|
542
|
+
// area lights — samples the light's solid angle directly, giving pdf = 1/S in
|
|
543
|
+
// solid-angle measure (much lower variance than uniform-area sampling for
|
|
544
|
+
// large/near lights). Inputs:
|
|
545
|
+
// o = shading point (ray origin)
|
|
546
|
+
// s = rectangle corner = center - u - v
|
|
547
|
+
// ex = full edge vector along u (= 2·u)
|
|
548
|
+
// ey = full edge vector along v (= 2·v)
|
|
549
|
+
// ================================================================================
|
|
550
|
+
|
|
551
|
+
// Solid angle S subtended by the rectangle from o (used for the BSDF-hit MIS pdf).
|
|
552
|
+
export const sphQuadSolidAngle = /*@__PURE__*/ wgslFn( `
|
|
553
|
+
fn sphQuadSolidAngle( o: vec3f, s: vec3f, ex: vec3f, ey: vec3f ) -> f32 {
|
|
554
|
+
let PI = 3.141592653589793f;
|
|
555
|
+
let exl = length( ex );
|
|
556
|
+
let eyl = length( ey );
|
|
557
|
+
let x = ex / max( exl, 1e-12f );
|
|
558
|
+
let y = ey / max( eyl, 1e-12f );
|
|
559
|
+
var z = cross( x, y );
|
|
560
|
+
let d = s - o;
|
|
561
|
+
var z0 = dot( d, z );
|
|
562
|
+
if ( z0 > 0.0f ) { z = -z; z0 = -z0; }
|
|
563
|
+
let x0 = dot( d, x );
|
|
564
|
+
let y0 = dot( d, y );
|
|
565
|
+
let x1 = x0 + exl;
|
|
566
|
+
let y1 = y0 + eyl;
|
|
567
|
+
let v00 = vec3f( x0, y0, z0 );
|
|
568
|
+
let v01 = vec3f( x0, y1, z0 );
|
|
569
|
+
let v10 = vec3f( x1, y0, z0 );
|
|
570
|
+
let v11 = vec3f( x1, y1, z0 );
|
|
571
|
+
let n0 = normalize( cross( v00, v10 ) );
|
|
572
|
+
let n1 = normalize( cross( v10, v11 ) );
|
|
573
|
+
let n2 = normalize( cross( v11, v01 ) );
|
|
574
|
+
let n3 = normalize( cross( v01, v00 ) );
|
|
575
|
+
let g0 = acos( clamp( -dot( n0, n1 ), -1.0f, 1.0f ) );
|
|
576
|
+
let g1 = acos( clamp( -dot( n1, n2 ), -1.0f, 1.0f ) );
|
|
577
|
+
let g2 = acos( clamp( -dot( n2, n3 ), -1.0f, 1.0f ) );
|
|
578
|
+
let g3 = acos( clamp( -dot( n3, n0 ), -1.0f, 1.0f ) );
|
|
579
|
+
return g0 + g1 + g2 + g3 - 2.0f * PI;
|
|
580
|
+
}
|
|
581
|
+
` );
|
|
582
|
+
|
|
583
|
+
// Sample a direction toward the rectangle uniformly in solid angle.
|
|
584
|
+
// Returns vec4( pointOnRect.xyz, pdf ); pdf = 1/S. pdf <= 0 signals the caller
|
|
585
|
+
// to fall back to uniform-area sampling (degenerate / tiny solid angle).
|
|
586
|
+
export const sampleSphQuad = /*@__PURE__*/ wgslFn( `
|
|
587
|
+
fn sampleSphQuad( o: vec3f, s: vec3f, ex: vec3f, ey: vec3f, uv: vec2f ) -> vec4f {
|
|
588
|
+
let PI = 3.141592653589793f;
|
|
589
|
+
let exl = length( ex );
|
|
590
|
+
let eyl = length( ey );
|
|
591
|
+
let x = ex / max( exl, 1e-12f );
|
|
592
|
+
let y = ey / max( eyl, 1e-12f );
|
|
593
|
+
var z = cross( x, y );
|
|
594
|
+
let d = s - o;
|
|
595
|
+
var z0 = dot( d, z );
|
|
596
|
+
if ( z0 > 0.0f ) { z = -z; z0 = -z0; }
|
|
597
|
+
let z0sq = z0 * z0;
|
|
598
|
+
let x0 = dot( d, x );
|
|
599
|
+
let y0 = dot( d, y );
|
|
600
|
+
let x1 = x0 + exl;
|
|
601
|
+
let y1 = y0 + eyl;
|
|
602
|
+
let y0sq = y0 * y0;
|
|
603
|
+
let y1sq = y1 * y1;
|
|
604
|
+
let v00 = vec3f( x0, y0, z0 );
|
|
605
|
+
let v01 = vec3f( x0, y1, z0 );
|
|
606
|
+
let v10 = vec3f( x1, y0, z0 );
|
|
607
|
+
let v11 = vec3f( x1, y1, z0 );
|
|
608
|
+
let n0 = normalize( cross( v00, v10 ) );
|
|
609
|
+
let n1 = normalize( cross( v10, v11 ) );
|
|
610
|
+
let n2 = normalize( cross( v11, v01 ) );
|
|
611
|
+
let n3 = normalize( cross( v01, v00 ) );
|
|
612
|
+
let g0 = acos( clamp( -dot( n0, n1 ), -1.0f, 1.0f ) );
|
|
613
|
+
let g1 = acos( clamp( -dot( n1, n2 ), -1.0f, 1.0f ) );
|
|
614
|
+
let g2 = acos( clamp( -dot( n2, n3 ), -1.0f, 1.0f ) );
|
|
615
|
+
let g3 = acos( clamp( -dot( n3, n0 ), -1.0f, 1.0f ) );
|
|
616
|
+
let b0 = n0.z;
|
|
617
|
+
let b1 = n2.z;
|
|
618
|
+
let b0sq = b0 * b0;
|
|
619
|
+
let k = 2.0f * PI - g2 - g3;
|
|
620
|
+
let S = g0 + g1 + g2 + g3 - 2.0f * PI;
|
|
621
|
+
|
|
622
|
+
if ( S <= 1e-5f ) {
|
|
623
|
+
return vec4f( 0.0f, 0.0f, 0.0f, -1.0f );
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let au = uv.x * S + k;
|
|
627
|
+
let sinAu = sin( au );
|
|
628
|
+
let fu = ( cos( au ) * b0 - b1 ) / max( abs( sinAu ), 1e-7f ) * sign( sinAu );
|
|
629
|
+
var cu = select( -1.0f, 1.0f, fu >= 0.0f ) / sqrt( fu * fu + b0sq );
|
|
630
|
+
cu = clamp( cu, -1.0f, 1.0f );
|
|
631
|
+
|
|
632
|
+
var xu = -( cu * z0 ) / max( sqrt( 1.0f - cu * cu ), 1e-7f );
|
|
633
|
+
xu = clamp( xu, x0, x1 );
|
|
634
|
+
|
|
635
|
+
let dd = sqrt( xu * xu + z0sq );
|
|
636
|
+
let h0 = y0 / sqrt( dd * dd + y0sq );
|
|
637
|
+
let h1 = y1 / sqrt( dd * dd + y1sq );
|
|
638
|
+
let hv = h0 + uv.y * ( h1 - h0 );
|
|
639
|
+
let hv2 = hv * hv;
|
|
640
|
+
var yv = y1;
|
|
641
|
+
if ( hv2 < 1.0f - 1e-6f ) {
|
|
642
|
+
yv = ( hv * dd ) / sqrt( 1.0f - hv2 );
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
let p = o + xu * x + yv * y + z0 * z;
|
|
646
|
+
return vec4f( p, 1.0f / S );
|
|
647
|
+
}
|
|
648
|
+
` );
|
|
649
|
+
|