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
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
sqrt,
|
|
30
30
|
cos,
|
|
31
31
|
sin,
|
|
32
|
+
tan,
|
|
32
33
|
dot,
|
|
33
34
|
cross,
|
|
34
35
|
normalize,
|
|
@@ -56,6 +57,8 @@ import {
|
|
|
56
57
|
sampleSpotGoboMask,
|
|
57
58
|
sampleDirectionalGoboMask,
|
|
58
59
|
sampleIESProfile,
|
|
60
|
+
sampleSphQuad,
|
|
61
|
+
sphQuadSolidAngle,
|
|
59
62
|
} from './LightsCore.js';
|
|
60
63
|
|
|
61
64
|
import { MISStrategy, DotProducts, DirectLightingDual } from './Struct.js';
|
|
@@ -90,7 +93,49 @@ const TWO_PI = 2.0 * PI;
|
|
|
90
93
|
// Light Sampling Functions
|
|
91
94
|
// =============================================================================
|
|
92
95
|
|
|
93
|
-
//
|
|
96
|
+
// Blender-style area-light spread: linearly attenuates emission as the receiver
|
|
97
|
+
// moves off the light's axis (gridded-softbox look). cosAngle = dot(-dir, normal),
|
|
98
|
+
// the emission cosine. spread = π (default) → full Lambertian hemisphere, no
|
|
99
|
+
// attenuation. Mirrors Cycles' area_light_spread_attenuation.
|
|
100
|
+
export const areaLightSpreadAttenuation = Fn( ( [ cosAngle, spread ] ) => {
|
|
101
|
+
|
|
102
|
+
const att = float( 1.0 ).toVar();
|
|
103
|
+
|
|
104
|
+
// Only attenuate for narrowed cones; at the π default tan(π/2)=∞ → skip.
|
|
105
|
+
If( spread.lessThan( float( PI ).sub( 1e-3 ) ).and( cosAngle.greaterThan( 0.0 ) ), () => {
|
|
106
|
+
|
|
107
|
+
const halfSpread = spread.mul( 0.5 ).toVar();
|
|
108
|
+
const tanHalf = tan( halfSpread ).toVar();
|
|
109
|
+
const sinA = sqrt( max( float( 1.0 ).sub( cosAngle.mul( cosAngle ) ), 0.0 ) );
|
|
110
|
+
const tanA = sinA.div( max( cosAngle, 1e-4 ) );
|
|
111
|
+
// Flux renormalization so narrowing the beam doesn't dim total power.
|
|
112
|
+
const normSpread = select(
|
|
113
|
+
halfSpread.greaterThan( 0.05 ),
|
|
114
|
+
float( 1.0 ).div( max( tanHalf.sub( halfSpread ), 1e-6 ) ),
|
|
115
|
+
float( 3.0 ).div( max( halfSpread.mul( halfSpread ).mul( halfSpread ), 1e-9 ) ),
|
|
116
|
+
);
|
|
117
|
+
att.assign( max( tanHalf.sub( tanA ).mul( normSpread ), 0.0 ) );
|
|
118
|
+
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
return att;
|
|
122
|
+
|
|
123
|
+
} );
|
|
124
|
+
|
|
125
|
+
// Emitted radiance for an area light (Blender/Cycles parity):
|
|
126
|
+
// L = color · power · (1/π) · (normalize ? 1/area : 1)
|
|
127
|
+
// The 1/π is the Lambertian emitter normalization; the 1/area (Normalize ON)
|
|
128
|
+
// keeps total radiant power constant as the light is resized.
|
|
129
|
+
export const areaLightRadiance = Fn( ( [ light ] ) => {
|
|
130
|
+
|
|
131
|
+
const invArea = select( light.normalize.greaterThan( 0.5 ), float( 1.0 ).div( max( light.area, 1e-10 ) ), float( 1.0 ) );
|
|
132
|
+
return light.color.mul( light.intensity ).mul( PI_INV ).mul( invArea );
|
|
133
|
+
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
// Area light sampling — rectangle uses spherical-rectangle (Ureña 2013)
|
|
137
|
+
// solid-angle sampling (pdf = 1/S); disk/ellipse and degenerate rects fall back
|
|
138
|
+
// to uniform-area sampling with the dist²/(area·cos) area→solid-angle pdf.
|
|
94
139
|
export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelectionPdf ] ) => {
|
|
95
140
|
|
|
96
141
|
// Result variables (no early return in TSL)
|
|
@@ -104,12 +149,49 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
|
|
|
104
149
|
// Validate light area to prevent NaN
|
|
105
150
|
If( light.area.greaterThan( 0.0 ), () => {
|
|
106
151
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
152
|
+
const sampledPos = vec3( 0.0 ).toVar();
|
|
153
|
+
// >0 → pdf already in solid-angle measure (spherical-rect); <0 → use area conversion.
|
|
154
|
+
const solidAnglePdf = float( - 1.0 ).toVar();
|
|
155
|
+
|
|
156
|
+
If( light.shape.greaterThan( 0.5 ), () => {
|
|
157
|
+
|
|
158
|
+
// Disk / ellipse: uniform sample on the unit disk, mapped through the
|
|
159
|
+
// half-vectors. PDF handled via the area→solid-angle conversion below.
|
|
160
|
+
const r = sqrt( ruv.x ).toVar();
|
|
161
|
+
const theta = float( TWO_PI ).mul( ruv.y ).toVar();
|
|
162
|
+
sampledPos.assign(
|
|
163
|
+
light.position
|
|
164
|
+
.add( light.u.mul( r.mul( cos( theta ) ) ) )
|
|
165
|
+
.add( light.v.mul( r.mul( sin( theta ) ) ) )
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
} ).Else( () => {
|
|
169
|
+
|
|
170
|
+
// Rectangle: spherical-rectangle solid-angle sampling.
|
|
171
|
+
const corner = light.position.sub( light.u ).sub( light.v );
|
|
172
|
+
const ex = light.u.mul( 2.0 );
|
|
173
|
+
const ey = light.v.mul( 2.0 );
|
|
174
|
+
const q = sampleSphQuad( rayOrigin, corner, ex, ey, ruv ).toVar();
|
|
175
|
+
|
|
176
|
+
If( q.w.greaterThan( 0.0 ), () => {
|
|
177
|
+
|
|
178
|
+
sampledPos.assign( q.xyz );
|
|
179
|
+
solidAnglePdf.assign( q.w );
|
|
180
|
+
|
|
181
|
+
} ).Else( () => {
|
|
182
|
+
|
|
183
|
+
// Degenerate / tiny solid angle → uniform-area fallback.
|
|
184
|
+
sampledPos.assign(
|
|
185
|
+
light.position
|
|
186
|
+
.add( light.u.mul( ruv.x.mul( 2.0 ).sub( 1.0 ) ) )
|
|
187
|
+
.add( light.v.mul( ruv.y.mul( 2.0 ).sub( 1.0 ) ) )
|
|
188
|
+
);
|
|
111
189
|
|
|
112
|
-
|
|
190
|
+
} );
|
|
191
|
+
|
|
192
|
+
} );
|
|
193
|
+
|
|
194
|
+
const toLight = sampledPos.sub( rayOrigin ).toVar();
|
|
113
195
|
const lightDistSq = dot( toLight, toLight ).toVar();
|
|
114
196
|
|
|
115
197
|
// Guard against zero distance
|
|
@@ -117,17 +199,17 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
|
|
|
117
199
|
|
|
118
200
|
const dist = sqrt( lightDistSq ).toVar();
|
|
119
201
|
const direction = toLight.div( dist ).toVar();
|
|
120
|
-
const
|
|
121
|
-
const cosAngle = dot( direction.negate(), lightNormal ).toVar();
|
|
202
|
+
const cosAngle = dot( direction.negate(), light.normal ).toVar();
|
|
122
203
|
|
|
123
204
|
ls_lightType.assign( int( LIGHT_TYPE_AREA ) );
|
|
124
|
-
ls_emission.assign( light.color.mul( light.intensity ) );
|
|
125
205
|
ls_distance.assign( dist );
|
|
126
206
|
ls_direction.assign( direction );
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
207
|
+
ls_emission.assign( areaLightRadiance( light ).mul( areaLightSpreadAttenuation( cosAngle, light.spread ) ) );
|
|
208
|
+
|
|
209
|
+
// Spherical-rect: pdf = 1/S (solid angle). Else: dist²/(area·cos).
|
|
210
|
+
const areaPdf = lightDistSq.div( max( light.area.mul( max( cosAngle, 0.001 ) ), 1e-10 ) );
|
|
211
|
+
const dirPdf = select( solidAnglePdf.greaterThan( 0.0 ), solidAnglePdf, areaPdf );
|
|
212
|
+
ls_pdf.assign( dirPdf.mul( lightSelectionPdf ) );
|
|
131
213
|
ls_valid.assign( cosAngle.greaterThan( 0.0 ) );
|
|
132
214
|
|
|
133
215
|
} );
|
|
@@ -679,6 +761,91 @@ export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material
|
|
|
679
761
|
|
|
680
762
|
} );
|
|
681
763
|
|
|
764
|
+
// Total light-selection weight at a shading point, replicating the NEE reservoir's
|
|
765
|
+
// importance accumulation (same per-type importance fns, same 16-light cap + order).
|
|
766
|
+
// Used to reconstruct the exact NEE selection pdf for MIS on the BSDF-hit path —
|
|
767
|
+
// the analogue of Cycles' light_tree_pdf re-walk. Without it, the BSDF-hit MIS
|
|
768
|
+
// partner assumes uniform 1/N selection, which disagrees with the importance-
|
|
769
|
+
// weighted NEE selection and biases the power-heuristic weights.
|
|
770
|
+
export const computeTotalLightImportance = Fn( ( [
|
|
771
|
+
rayOrigin, normal, material, bounceIndex,
|
|
772
|
+
directionalLightsBuffer, numDirectionalLights,
|
|
773
|
+
areaLightsBuffer, numAreaLights,
|
|
774
|
+
pointLightsBuffer, numPointLights,
|
|
775
|
+
spotLightsBuffer, numSpotLights,
|
|
776
|
+
] ) => {
|
|
777
|
+
|
|
778
|
+
const totalWeight = float( 0.0 ).toVar();
|
|
779
|
+
const lightIndex = int( 0 ).toVar();
|
|
780
|
+
|
|
781
|
+
If( numDirectionalLights.greaterThan( int( 0 ) ), () => {
|
|
782
|
+
|
|
783
|
+
Loop( { start: int( 0 ), end: numDirectionalLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
784
|
+
|
|
785
|
+
If( lightIndex.lessThan( int( 16 ) ), () => {
|
|
786
|
+
|
|
787
|
+
const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
|
|
788
|
+
totalWeight.addAssign( calculateDirectionalLightImportance( light, normal, material, bounceIndex ) );
|
|
789
|
+
lightIndex.addAssign( 1 );
|
|
790
|
+
|
|
791
|
+
} );
|
|
792
|
+
|
|
793
|
+
} );
|
|
794
|
+
|
|
795
|
+
} );
|
|
796
|
+
|
|
797
|
+
If( numAreaLights.greaterThan( int( 0 ) ), () => {
|
|
798
|
+
|
|
799
|
+
Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
800
|
+
|
|
801
|
+
If( lightIndex.lessThan( int( 16 ) ), () => {
|
|
802
|
+
|
|
803
|
+
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
|
|
804
|
+
totalWeight.addAssign( select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ) );
|
|
805
|
+
lightIndex.addAssign( 1 );
|
|
806
|
+
|
|
807
|
+
} );
|
|
808
|
+
|
|
809
|
+
} );
|
|
810
|
+
|
|
811
|
+
} );
|
|
812
|
+
|
|
813
|
+
If( numPointLights.greaterThan( int( 0 ) ), () => {
|
|
814
|
+
|
|
815
|
+
Loop( { start: int( 0 ), end: numPointLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
816
|
+
|
|
817
|
+
If( lightIndex.lessThan( int( 16 ) ), () => {
|
|
818
|
+
|
|
819
|
+
const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
|
|
820
|
+
totalWeight.addAssign( calculatePointLightImportance( light, rayOrigin, normal, material ) );
|
|
821
|
+
lightIndex.addAssign( 1 );
|
|
822
|
+
|
|
823
|
+
} );
|
|
824
|
+
|
|
825
|
+
} );
|
|
826
|
+
|
|
827
|
+
} );
|
|
828
|
+
|
|
829
|
+
If( numSpotLights.greaterThan( int( 0 ) ), () => {
|
|
830
|
+
|
|
831
|
+
Loop( { start: int( 0 ), end: numSpotLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
832
|
+
|
|
833
|
+
If( lightIndex.lessThan( int( 16 ) ), () => {
|
|
834
|
+
|
|
835
|
+
const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
|
|
836
|
+
totalWeight.addAssign( calculateSpotLightImportance( light, rayOrigin, normal, material ) );
|
|
837
|
+
lightIndex.addAssign( 1 );
|
|
838
|
+
|
|
839
|
+
} );
|
|
840
|
+
|
|
841
|
+
} );
|
|
842
|
+
|
|
843
|
+
} );
|
|
844
|
+
|
|
845
|
+
return totalWeight;
|
|
846
|
+
|
|
847
|
+
} );
|
|
848
|
+
|
|
682
849
|
// =============================================================================
|
|
683
850
|
// Unified Direct Lighting System
|
|
684
851
|
// =============================================================================
|
|
@@ -936,15 +1103,41 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
936
1103
|
If( lightFacing.greaterThan( 0.0 ), () => {
|
|
937
1104
|
|
|
938
1105
|
const lightDistSq = hitDistance.mul( hitDistance );
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1106
|
+
|
|
1107
|
+
// Directional pdf must match the NEE sampler: 1/S (spherical-rect)
|
|
1108
|
+
// for rectangles, dist²/(area·cos) for disk/ellipse + degenerate rects.
|
|
1109
|
+
const corner = light.position.sub( light.u ).sub( light.v );
|
|
1110
|
+
const sAngle = sphQuadSolidAngle( rayOrigin, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
|
|
1111
|
+
const areaPdf = lightDistSq.div( max( light.area.mul( lightFacing ), EPSILON ) );
|
|
1112
|
+
const dirPdf = select(
|
|
1113
|
+
light.shape.lessThan( 0.5 ).and( sAngle.greaterThan( 1e-5 ) ),
|
|
1114
|
+
float( 1.0 ).div( max( sAngle, 1e-10 ) ),
|
|
1115
|
+
areaPdf,
|
|
1116
|
+
).toVar();
|
|
1117
|
+
|
|
1118
|
+
// Selection pdf consistent with the importance-weighted NEE reservoir
|
|
1119
|
+
// (Cycles light_tree_pdf re-walk). Falls back to uniform 1/N only when
|
|
1120
|
+
// total importance is zero, matching the NEE fallback.
|
|
1121
|
+
const selImp = estimateLightImportance( light, rayOrigin, hitNormal, material );
|
|
1122
|
+
const totalImp = computeTotalLightImportance(
|
|
1123
|
+
rayOrigin, hitNormal, material, bounceIndex,
|
|
1124
|
+
directionalLightsBuffer, numDirectionalLights,
|
|
1125
|
+
areaLightsBuffer, numAreaLights,
|
|
1126
|
+
pointLightsBuffer, numPointLights,
|
|
1127
|
+
spotLightsBuffer, numSpotLights,
|
|
1128
|
+
).toVar();
|
|
1129
|
+
const selPdf = select(
|
|
1130
|
+
totalImp.greaterThan( 0.0 ),
|
|
1131
|
+
selImp.div( max( totalImp, 1e-10 ) ),
|
|
1132
|
+
float( 1.0 ).div( max( float( totalLights ), 1.0 ) ),
|
|
1133
|
+
);
|
|
1134
|
+
const lightPdf = dirPdf.mul( selPdf ).toVar();
|
|
942
1135
|
|
|
943
1136
|
const brdfPdfWeighted = brdfSamplePdf.mul( brdfWeight );
|
|
944
1137
|
const lightPdfWeighted = lightPdf.mul( lightWeight );
|
|
945
1138
|
const misW = powerHeuristic( { pdf1: brdfPdfWeighted, pdf2: lightPdfWeighted } ).toVar();
|
|
946
1139
|
|
|
947
|
-
const lightEmission = light.
|
|
1140
|
+
const lightEmission = areaLightRadiance( light ).mul( areaLightSpreadAttenuation( lightFacing, light.spread ) );
|
|
948
1141
|
// Base contribution WITHOUT visibility (see discrete-light site).
|
|
949
1142
|
const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( brdfWeight, 1e-10 ) );
|
|
950
1143
|
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SortGlobalKernels.js — GLOBAL material counting sort of the entering rays.
|
|
3
|
+
*
|
|
4
|
+
* Unlike a per-workgroup sort (locally coherent only), this orders rays by material
|
|
5
|
+
* ACROSS ALL workgroups, so each material's rays occupy one contiguous region of sortedIndices.
|
|
6
|
+
* For materials with many rays that means whole material-PURE workgroups/subgroups → real shading
|
|
7
|
+
* coherence. MEASURED −8% vs no-sort at 1024²/8b (per-WG sort was neutral).
|
|
8
|
+
*
|
|
9
|
+
* Four passes (counting sort): reset → histogram → exclusive prefix sum → scatter.
|
|
10
|
+
* - histogram uses a per-workgroup local histogram (workgroup-shared atomics, patches.js §4)
|
|
11
|
+
* flushed to the global histogram once per bin per workgroup, cutting global-atomic contention
|
|
12
|
+
* from O(rays) to O(workgroups × bins).
|
|
13
|
+
* - the prefix-summed histogram doubles as the per-bin running cursor in scatter.
|
|
14
|
+
*
|
|
15
|
+
* `bins` is chosen per-scene by the caller (= material count, capped). Materials ≥ bins clamp into
|
|
16
|
+
* the last bin (graceful coherence loss). Capped at SORT_GLOBAL_MAX_BINS so a single 256-thread
|
|
17
|
+
* workgroup can zero/flush exactly one bin per thread (no per-thread loop needed).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
Fn, uint,
|
|
22
|
+
If, Loop,
|
|
23
|
+
instanceIndex, localId,
|
|
24
|
+
workgroupBarrier,
|
|
25
|
+
atomicAdd, atomicLoad, atomicStore,
|
|
26
|
+
} from 'three/tsl';
|
|
27
|
+
|
|
28
|
+
import { workgroupAtomicArray } from './patches.js';
|
|
29
|
+
import { readHitMaterialIndex } from '../Processor/PackedRayBuffer.js';
|
|
30
|
+
import { COUNTER } from '../Processor/QueueManager.js';
|
|
31
|
+
|
|
32
|
+
const WG_SIZE = 256;
|
|
33
|
+
export const SORT_GLOBAL_MAX_BINS = 256;
|
|
34
|
+
|
|
35
|
+
// Pass 1 — zero the global histogram.
|
|
36
|
+
export function buildResetGlobalHistKernel( { sortGlobalHistogram, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
37
|
+
|
|
38
|
+
return Fn( () => {
|
|
39
|
+
|
|
40
|
+
If( instanceIndex.lessThan( uint( bins ) ), () => {
|
|
41
|
+
|
|
42
|
+
atomicStore( sortGlobalHistogram.element( instanceIndex ), uint( 0 ) );
|
|
43
|
+
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pass 2 — histogram: per-workgroup local count (on-chip), flushed to global once per bin/WG.
|
|
51
|
+
export function buildGlobalHistKernel( { hitBufferRO, activeIndicesReadRO, sortGlobalHistogram, counters, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
52
|
+
|
|
53
|
+
return Fn( () => {
|
|
54
|
+
|
|
55
|
+
const tid = instanceIndex;
|
|
56
|
+
const lid = localId.x;
|
|
57
|
+
const local = workgroupAtomicArray( 'uint', bins );
|
|
58
|
+
|
|
59
|
+
If( lid.lessThan( uint( bins ) ), () => {
|
|
60
|
+
|
|
61
|
+
atomicStore( local.element( lid ), uint( 0 ) );
|
|
62
|
+
|
|
63
|
+
} );
|
|
64
|
+
workgroupBarrier();
|
|
65
|
+
|
|
66
|
+
const entering = atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) );
|
|
67
|
+
If( tid.lessThan( entering ), () => {
|
|
68
|
+
|
|
69
|
+
const rayID = activeIndicesReadRO.element( tid );
|
|
70
|
+
const bin = uint( readHitMaterialIndex( hitBufferRO, rayID ) ).clamp( uint( 0 ), uint( bins - 1 ) );
|
|
71
|
+
atomicAdd( local.element( bin ), uint( 1 ) );
|
|
72
|
+
|
|
73
|
+
} );
|
|
74
|
+
workgroupBarrier();
|
|
75
|
+
|
|
76
|
+
If( lid.lessThan( uint( bins ) ), () => {
|
|
77
|
+
|
|
78
|
+
const c = atomicLoad( local.element( lid ) );
|
|
79
|
+
If( c.greaterThan( uint( 0 ) ), () => {
|
|
80
|
+
|
|
81
|
+
atomicAdd( sortGlobalHistogram.element( lid ), c );
|
|
82
|
+
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pass 3 — exclusive prefix sum over global bins (single thread; dispatch [1,1,1]).
|
|
92
|
+
export function buildGlobalPrefixKernel( { sortGlobalHistogram, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
93
|
+
|
|
94
|
+
return Fn( () => {
|
|
95
|
+
|
|
96
|
+
If( instanceIndex.equal( uint( 0 ) ), () => {
|
|
97
|
+
|
|
98
|
+
const running = uint( 0 ).toVar();
|
|
99
|
+
Loop( { start: uint( 0 ), end: uint( bins ), type: 'uint' }, ( { i } ) => {
|
|
100
|
+
|
|
101
|
+
const slot = sortGlobalHistogram.element( i );
|
|
102
|
+
const c = atomicLoad( slot );
|
|
103
|
+
atomicStore( slot, running );
|
|
104
|
+
running.addAssign( c );
|
|
105
|
+
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Pass 4 — scatter: atomicAdd on the prefix-summed histogram gives each ray its global slot
|
|
115
|
+
// within its material's contiguous region.
|
|
116
|
+
export function buildGlobalScatterKernel( { hitBufferRO, activeIndicesReadRO, sortedIndicesRW, sortGlobalHistogram, counters, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
117
|
+
|
|
118
|
+
return Fn( () => {
|
|
119
|
+
|
|
120
|
+
const tid = instanceIndex;
|
|
121
|
+
const entering = atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) );
|
|
122
|
+
If( tid.lessThan( entering ), () => {
|
|
123
|
+
|
|
124
|
+
const rayID = activeIndicesReadRO.element( tid );
|
|
125
|
+
const bin = uint( readHitMaterialIndex( hitBufferRO, rayID ) ).clamp( uint( 0 ), uint( bins - 1 ) );
|
|
126
|
+
const pos = atomicAdd( sortGlobalHistogram.element( bin ), uint( 1 ) );
|
|
127
|
+
sortedIndicesRW.element( pos ).assign( rayID );
|
|
128
|
+
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { WG_SIZE as SORT_GLOBAL_WG_SIZE };
|
package/src/TSL/patches.js
CHANGED
|
@@ -61,6 +61,10 @@ WebGPUBackend.prototype.createNodeBuilder = function ( object, renderer ) {
|
|
|
61
61
|
configurable: true,
|
|
62
62
|
} );
|
|
63
63
|
|
|
64
|
+
// Install the workgroup-atomic-array codegen patch (section 4) lazily off the
|
|
65
|
+
// first builder, so we don't need to import WGSLNodeBuilder directly.
|
|
66
|
+
_installScopedArrayAtomicPatch( builder );
|
|
67
|
+
|
|
64
68
|
return builder;
|
|
65
69
|
|
|
66
70
|
};
|
|
@@ -220,3 +224,89 @@ export function struct( members, name = null ) {
|
|
|
220
224
|
return wrappedFactory;
|
|
221
225
|
|
|
222
226
|
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// 4. Workgroup-scoped atomic arrays — var<workgroup> array<atomic<T>, N>
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// TSL's `workgroupArray(type, count)` emits `var<workgroup> name: array<T, N>`
|
|
232
|
+
// (`WGSLNodeBuilder.getScopedArrays`, r185 ~line 1773) with a NON-atomic element
|
|
233
|
+
// type, so `atomicAdd(arr.element(i), v)` fails WGSL validation. AtomicFunctionNode
|
|
234
|
+
// is itself address-space-agnostic — it emits `atomicAdd(&name[i], v)`, valid for
|
|
235
|
+
// BOTH storage and workgroup pointers — so the ONLY missing piece is the
|
|
236
|
+
// declaration. This patch adds `workgroupAtomicArray()` (a `workgroupArray` tagged
|
|
237
|
+
// atomic) and overrides `getScopedArrays` to wrap the element type in `atomic<…>`
|
|
238
|
+
// for tagged arrays, enabling true on-chip workgroup-shared atomics (e.g. fast
|
|
239
|
+
// per-workgroup histograms) instead of the slow global-storage-atomic fallback.
|
|
240
|
+
// Access tagged arrays ONLY via atomic ops (atomicAdd/atomicLoad/atomicStore).
|
|
241
|
+
|
|
242
|
+
import { workgroupArray } from 'three/tsl';
|
|
243
|
+
|
|
244
|
+
const _WorkgroupInfoNode = workgroupArray( 'uint', 1 ).constructor;
|
|
245
|
+
|
|
246
|
+
// Tag the node so the patched getScopedArrays emits an atomic element type. The
|
|
247
|
+
// array's WGSL name is only known after generate() runs, so we mark the builder's
|
|
248
|
+
// scopedArrays entry there (idempotent across analyze/generate passes).
|
|
249
|
+
const _origWorkgroupGenerate = _WorkgroupInfoNode.prototype.generate;
|
|
250
|
+
|
|
251
|
+
_WorkgroupInfoNode.prototype.generate = function ( builder ) {
|
|
252
|
+
|
|
253
|
+
const name = _origWorkgroupGenerate.call( this, builder );
|
|
254
|
+
if ( this.isAtomicArray === true ) {
|
|
255
|
+
|
|
256
|
+
const entry = builder.scopedArrays && builder.scopedArrays.get( name );
|
|
257
|
+
if ( entry ) entry.isAtomic = true;
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return name;
|
|
262
|
+
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Like `workgroupArray(type, count)` but declares the workgroup buffer with an
|
|
267
|
+
* atomic element type: `var<workgroup> name: array<atomic<type>, count>`.
|
|
268
|
+
* Elements MUST be accessed only via atomic ops (atomicAdd/atomicLoad/atomicStore).
|
|
269
|
+
*
|
|
270
|
+
* @param {string} type - Element type (e.g. 'uint').
|
|
271
|
+
* @param {number} count - Number of elements.
|
|
272
|
+
* @returns {WorkgroupInfoNode} The tagged workgroup array node.
|
|
273
|
+
*/
|
|
274
|
+
export function workgroupAtomicArray( type, count ) {
|
|
275
|
+
|
|
276
|
+
const node = workgroupArray( type, count );
|
|
277
|
+
node.isAtomicArray = true;
|
|
278
|
+
return node;
|
|
279
|
+
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let _scopedArraysPatched = false;
|
|
283
|
+
|
|
284
|
+
// Override getScopedArrays on the builder's prototype (compute-stage WGSL
|
|
285
|
+
// assembly) to emit `atomic<T>` element types for tagged workgroup arrays.
|
|
286
|
+
// Installed once, lazily, off the first node builder created.
|
|
287
|
+
function _installScopedArrayAtomicPatch( builder ) {
|
|
288
|
+
|
|
289
|
+
if ( _scopedArraysPatched ) return;
|
|
290
|
+
const proto = Object.getPrototypeOf( builder );
|
|
291
|
+
if ( ! proto || typeof proto.getScopedArrays !== 'function' ) return;
|
|
292
|
+
|
|
293
|
+
proto.getScopedArrays = function ( shaderStage ) {
|
|
294
|
+
|
|
295
|
+
if ( shaderStage !== 'compute' ) return;
|
|
296
|
+
|
|
297
|
+
const snippets = [];
|
|
298
|
+
for ( const { name, scope, bufferType, bufferCount, isAtomic } of this.scopedArrays.values() ) {
|
|
299
|
+
|
|
300
|
+
const type = this.getType( bufferType );
|
|
301
|
+
const elementType = ( isAtomic === true ) ? `atomic< ${ type } >` : type;
|
|
302
|
+
snippets.push( `var<${ scope }> ${ name }: array< ${ elementType }, ${ bufferCount } >;` );
|
|
303
|
+
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return snippets.join( '\n' );
|
|
307
|
+
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
_scopedArraysPatched = true;
|
|
311
|
+
|
|
312
|
+
}
|
|
@@ -38,10 +38,11 @@ export class LightManager extends EventDispatcher {
|
|
|
38
38
|
addLight( type ) {
|
|
39
39
|
|
|
40
40
|
const defaults = {
|
|
41
|
+
// Power in Watts (Blender-style) for point/spot/area; Sun is W/m² strength.
|
|
41
42
|
DirectionalLight: { position: [ 1, 1, 1 ], intensity: 1.0, color: '#ffffff' },
|
|
42
|
-
PointLight: { position: [ 0, 2, 0 ], intensity:
|
|
43
|
-
SpotLight: { position: [ 0, 1, 0 ], intensity:
|
|
44
|
-
RectAreaLight: { position: [ 0, 2, 0 ], intensity:
|
|
43
|
+
PointLight: { position: [ 0, 2, 0 ], intensity: 1000, color: '#ffffff' },
|
|
44
|
+
SpotLight: { position: [ 0, 1, 0 ], intensity: 1000, color: '#ffffff', angle: 15 },
|
|
45
|
+
RectAreaLight: { position: [ 0, 2, 0 ], intensity: 100, color: '#ffffff', width: 2, height: 2 }
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
const props = defaults[ type ];
|
|
@@ -73,9 +74,19 @@ export class LightManager extends EventDispatcher {
|
|
|
73
74
|
light = new RectAreaLight( props.color, props.intensity, props.width, props.height );
|
|
74
75
|
light.position.fromArray( props.position );
|
|
75
76
|
light.lookAt( 0, 0, 0 );
|
|
77
|
+
// Blender-style emission defaults: power-normalized, full Lambertian
|
|
78
|
+
// hemisphere (spread = π), rectangular shape.
|
|
79
|
+
light.userData.normalize = true;
|
|
80
|
+
light.userData.spread = Math.PI;
|
|
81
|
+
light.userData.shape = 'rectangle';
|
|
76
82
|
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
// Blender-style emission controls common to every light type.
|
|
86
|
+
light.userData.temperature = 6500;
|
|
87
|
+
light.userData.useTemperature = false;
|
|
88
|
+
light.userData.exposure = 0;
|
|
89
|
+
|
|
79
90
|
const count = this.scene.getObjectsByProperty( 'isLight', true ).length;
|
|
80
91
|
light.name = `${type.replace( 'Light', '' )} ${count + 1}`;
|
|
81
92
|
this.scene.add( light );
|
|
@@ -322,13 +333,24 @@ export class LightManager extends EventDispatcher {
|
|
|
322
333
|
intensity: light.intensity,
|
|
323
334
|
color: `#${light.color.getHexString()}`,
|
|
324
335
|
position: [ light.position.x, light.position.y, light.position.z ],
|
|
325
|
-
angle
|
|
336
|
+
angle,
|
|
337
|
+
// Emission controls common to all light types.
|
|
338
|
+
temperature: light.userData?.temperature ?? 6500,
|
|
339
|
+
useTemperature: light.userData?.useTemperature ?? false,
|
|
340
|
+
exposure: light.userData?.exposure ?? 0
|
|
326
341
|
};
|
|
327
342
|
|
|
328
343
|
if ( light.type === 'RectAreaLight' ) {
|
|
329
344
|
|
|
330
345
|
descriptor.width = light.width;
|
|
331
346
|
descriptor.height = light.height;
|
|
347
|
+
descriptor.normalize = light.userData?.normalize ?? true;
|
|
348
|
+
descriptor.spread = MathUtils.radToDeg( light.userData?.spread ?? Math.PI ); // degrees for UI
|
|
349
|
+
const rawShape = light.userData?.shape;
|
|
350
|
+
descriptor.shape = ( rawShape === 'square' || rawShape === 'rectangle' || rawShape === 'disk' || rawShape === 'ellipse' )
|
|
351
|
+
? rawShape
|
|
352
|
+
: rawShape === 1 ? 'ellipse'
|
|
353
|
+
: 'rectangle'; // 'rect', undefined, 0 → rectangle
|
|
332
354
|
const dir = light.getWorldDirection( light.position.clone() );
|
|
333
355
|
descriptor.target = [ light.position.x + dir.x, light.position.y + dir.y, light.position.z + dir.z ];
|
|
334
356
|
|
|
@@ -212,7 +212,7 @@ export class UniformManager {
|
|
|
212
212
|
// Light buffer nodes - pre-allocate for up to 16 lights per type (shader hard cap)
|
|
213
213
|
this._lightBuffers = {
|
|
214
214
|
directional: uniformArray( new Float32Array( 12 * 16 ), 'float' ),
|
|
215
|
-
area: uniformArray( new Float32Array(
|
|
215
|
+
area: uniformArray( new Float32Array( 16 * 16 ), 'float' ),
|
|
216
216
|
point: uniformArray( new Float32Array( 9 * 16 ), 'float' ),
|
|
217
217
|
spot: uniformArray( new Float32Array( 20 * 16 ), 'float' ),
|
|
218
218
|
};
|