rayzee 9.4.1 → 9.5.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 +226 -25
- package/dist/ActiveColor-YvuQq44O.js +13 -0
- package/dist/ArchiveImporter-xuUJiklP.js +4301 -0
- package/dist/AssetConfig-DSEntE65.js +30 -0
- package/dist/BidirectionalIntegrator-C5PKjJoT.js +934 -0
- package/dist/ColorManagement-CheBCPaW.js +1488 -0
- package/dist/EngineDefaults-DH3bI5t-.js +844 -0
- package/dist/EngineEvents-Ci8dVWCP.js +73 -0
- package/dist/EngineIssues-CIZnXHsF.js +72 -0
- package/dist/FileFormats-sPGJeCRc.js +102 -0
- package/dist/Logger-BnHghprG.js +97 -0
- package/dist/{NeuralRetouch-vkdXXyK2.js → NeuralRetouch-CW11yLZ_.js} +8 -6
- package/dist/{NeuralSuperRes-FVqRJKQV.js → NeuralSuperRes-BekaecYg.js} +55 -120
- package/dist/PathTracer-Ccl9KOuF.js +29729 -0
- package/dist/PhysicalSky-Dv2PMFGW.js +1219 -0
- package/dist/RayzeeRenderer-BQxnzCTD.js +3542 -0
- package/dist/SolarLimb-ken-JbDK.js +721 -0
- package/dist/ToneMapGPU-BHi5JzIq.js +177 -0
- package/dist/ViewTransforms-D0rTLBsX.js +255 -0
- package/dist/WorkingMatrix-C6y233hT.js +27 -0
- package/dist/ZipReader-gDSWr0I5.js +98 -0
- package/dist/addons/archives.es.js +3 -0
- package/dist/addons/bidirectional.es.js +2 -0
- package/dist/addons/color.es.js +3 -0
- package/dist/addons/formats.es.js +2 -0
- package/dist/addons/physical-sky.es.js +2 -0
- package/dist/addons/storage.es.js +3 -0
- package/dist/archiveFormats-BZw7TiDp.js +75 -0
- package/dist/identity-DrQsGT2o.js +39 -0
- package/dist/openStorage-DwcBRUj7.js +820 -0
- package/dist/rayzee-core.es.js +12 -0
- package/dist/rayzee.es.js +2266 -44125
- package/dist/rayzee.umd.js +485 -473
- package/dist/sceneCachePolicy-BmwYOYDu.js +183 -0
- package/dist/shared-BGBcSxBQ.js +17 -0
- package/package.json +23 -2
- package/src/Color/ActiveColor.js +34 -0
- package/src/Color/BasicColor.js +147 -0
- package/src/Color/ColorManagement.js +7 -31
- package/src/EngineDefaults.js +23 -14
- package/src/EngineEvents.js +2 -0
- package/src/EngineIssues.js +1 -0
- package/src/PathTracerApp.js +617 -4016
- package/src/Pipeline/PipelineContext.js +0 -4
- package/src/Platform.js +36 -0
- package/src/Processor/ArchiveCache.js +1 -1
- package/src/Processor/ArchiveImporter.js +1259 -0
- package/src/Processor/AssetLoader.js +322 -1746
- package/src/Processor/AtmosphereModel.js +1 -2
- package/src/Processor/BVHBuilder.js +4 -95
- package/src/Processor/EnvironmentExactTable.js +187 -0
- package/src/Processor/EquirectHDRInfo.js +30 -223
- package/src/Processor/FileFormats.js +116 -0
- package/src/Processor/GLTFDecoders.js +85 -0
- package/src/Processor/GLTFDiffuseTransmission.js +63 -0
- package/src/Processor/GeometryExtractor.js +12 -1
- package/src/Processor/IESParser.js +47 -35
- package/src/Processor/KernelManager.js +35 -2
- package/src/Processor/MaterialPacking.js +8 -7
- package/src/Processor/PBRT/PBRTMaterials.js +129 -41
- package/src/Processor/PBRT/PBRTParser.js +30 -17
- package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
- package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
- package/src/Processor/PBRT/PFM.js +59 -0
- package/src/Processor/PackedRayBuffer.js +116 -9
- package/src/Processor/ParallelBVHBuilder.js +3 -6
- package/src/Processor/PhysicalSky.js +5 -4
- package/src/Processor/PlatformImageLoader.js +50 -6
- package/src/Processor/QueueManager.js +12 -3
- package/src/Processor/ReinsertionOptimizer.js +2 -2
- package/src/Processor/SceneProcessor.js +15 -55
- package/src/Processor/ShaderBuilder.js +6 -16
- package/src/Processor/SimpleSky.js +1 -62
- package/src/Processor/SolarLimb.js +3 -0
- package/src/Processor/TextureCreator.js +1 -1
- package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
- package/src/Processor/Workers/BVHWorker.js +5 -18
- package/src/Processor/Workers/CDFWorker.js +6 -183
- package/src/Processor/archiveFormats.js +7 -0
- package/src/Processor/utils.js +2 -1
- package/src/RayzeeRenderer.js +3902 -0
- package/src/RenderSettings.js +49 -41
- package/src/Stages/ASVGF.js +5 -7
- package/src/Stages/AutoExposure.js +3 -12
- package/src/Stages/Compositor.js +8 -23
- package/src/Stages/NRD.js +9 -2
- package/src/Stages/NormalDepth.js +9 -6
- package/src/Stages/PathTracer.js +254 -38
- package/src/Stages/PathTracerStage.js +11 -16
- package/src/Storage/BLASCache.js +1 -1
- package/src/Storage/CDFCache.js +18 -17
- package/src/Storage/DownloadCache.js +1 -1
- package/src/Storage/SceneGraphCodec.js +46 -3
- package/src/Storage/SpillStore.js +1 -1
- package/src/Storage/StorageManager.js +2 -4
- package/src/Storage/areas.js +4 -0
- package/src/TSL/BVHTraversal.js +62 -24
- package/src/TSL/Bidirectional.js +204 -0
- package/src/TSL/BidirectionalLamps.js +132 -0
- package/src/TSL/Clearcoat.js +15 -6
- package/src/TSL/Common.js +19 -14
- package/src/TSL/ConnectKernel.js +147 -0
- package/src/TSL/Debugger.js +1 -1
- package/src/TSL/Displacement.js +4 -3
- package/src/TSL/EmissiveSampling.js +64 -17
- package/src/TSL/Environment.js +77 -106
- package/src/TSL/EnvironmentCDF.js +85 -39
- package/src/TSL/ExtendKernel.js +8 -3
- package/src/TSL/FinalWriteKernel.js +4 -4
- package/src/TSL/GenerateKernel.js +18 -4
- package/src/TSL/HitFacet.js +35 -5
- package/src/TSL/LightBVHSampling.js +40 -9
- package/src/TSL/LightGenerateKernel.js +293 -0
- package/src/TSL/LightGuide.js +198 -0
- package/src/TSL/LightSplatKernel.js +165 -0
- package/src/TSL/LightsCore.js +13 -38
- package/src/TSL/LightsDirect.js +28 -29
- package/src/TSL/LightsIndirect.js +3 -1
- package/src/TSL/LightsSampling.js +90 -38
- package/src/TSL/MaterialEvaluation.js +113 -31
- package/src/TSL/MaterialProperties.js +44 -17
- package/src/TSL/MaterialTransmission.js +52 -47
- package/src/TSL/MergeKernel.js +218 -0
- package/src/TSL/PathTracerCore.js +65 -29
- package/src/TSL/Random.js +4 -0
- package/src/TSL/SceneResources.js +51 -0
- package/src/TSL/ShadeKernel.js +881 -103
- package/src/TSL/Struct.js +10 -0
- package/src/TSL/Sun.js +1 -1
- package/src/TSL/TextureSampling.js +65 -60
- package/src/addons/archives.js +14 -0
- package/src/addons/bidirectional.js +13 -0
- package/src/addons/color.js +32 -0
- package/src/addons/formats.js +16 -0
- package/src/addons/physicalSky.js +14 -0
- package/src/addons/storage.js +19 -0
- package/src/core.js +35 -0
- package/src/index.js +3 -1
- package/src/integrators/BidirectionalIntegrator.js +832 -0
- package/src/managers/EnvironmentManager.js +94 -91
- package/src/managers/MaterialDataManager.js +42 -0
- package/src/managers/UniformManager.js +0 -1
- package/dist/ToneMapGPU-DKQM1pMd.js +0 -552
- package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
- package/src/Processor/TreeletOptimizer.js +0 -574
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bidirectional path tracing, and vertex merging on top of it — the path tracer's 'bidirectional' and 'vcm'
|
|
3
|
+
* integrators, an add-on (rayzee/addons/bidirectional). The path tracer calls it at fixed points of a frame (its
|
|
4
|
+
* "Integrator hooks"): light subpaths before the camera chunks, connections and merges after each Shade, the light
|
|
5
|
+
* tracing image before FinalWrite. Shade and Generate receive `uniforms`, whose `lib` carries the bidirectional
|
|
6
|
+
* shading functions, so the core imports none of them. See CLAUDE.md, "Bidirectional integrator".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { uniform, uniformArray, texture, storage, Fn, uint, atomicStore, atomicLoad, instanceIndex, If, Return } from 'three/tsl';
|
|
10
|
+
import { DataTexture, FloatType, RedFormat, Vector3 } from 'three';
|
|
11
|
+
import { gpuOnlyStorageAttribute } from '../TSL/patches.js';
|
|
12
|
+
import { LIGHT_FLOATS } from '../managers/UniformManager.js';
|
|
13
|
+
import { HIT_STRIDE_BIDIRECTIONAL, LIGHT_VERTEX_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
|
|
14
|
+
import { COUNTER, ENERGY_SCALE } from '../Processor/QueueManager.js';
|
|
15
|
+
import { CAMERA_PROJECTION_IDS, BVH_MAX_INDEX } from '../EngineDefaults.js';
|
|
16
|
+
import { BOUNCE_KERNELS, LIST_WG_SIZE } from '../Stages/PathTracer.js';
|
|
17
|
+
import { buildLightGenerateKernel, LIGHT_GENERATE_WG_SIZE } from '../TSL/LightGenerateKernel.js';
|
|
18
|
+
import { buildGuideKernel, buildGuideClearKernel, guidedDiscPdf, recordEscape, GUIDE_BINS, GUIDE_ROW_STRIDE, GUIDE_TEXTURE_WIDTH } from '../TSL/LightGuide.js';
|
|
19
|
+
import { buildConnectKernel, CONNECT_WG_SIZE } from '../TSL/ConnectKernel.js';
|
|
20
|
+
import { buildMergeClearKernel, buildMergeInsertKernel, buildMergeKernel, MERGE_WG_SIZE } from '../TSL/MergeKernel.js';
|
|
21
|
+
import { buildLightSplatKernel, buildSplatResolveKernel, LIGHT_SPLAT_WG_SIZE, SPLAT_RESOLVE_WG_SIZE } from '../TSL/LightSplatKernel.js';
|
|
22
|
+
import * as Bidirectional from '../TSL/Bidirectional.js';
|
|
23
|
+
import * as BidirectionalLamps from '../TSL/BidirectionalLamps.js';
|
|
24
|
+
|
|
25
|
+
const { PASS_TAG_BIT, STRATEGY, STRATEGY_ALONE, SOURCE, mergeVmAt } = Bidirectional;
|
|
26
|
+
|
|
27
|
+
// What Shade and Generate call when the integrator is on: they take it from `uniforms.lib`.
|
|
28
|
+
const SHADING_LIB = { ...Bidirectional, ...BidirectionalLamps, guidedDiscPdf, recordEscape };
|
|
29
|
+
|
|
30
|
+
// Light vertex cache for the bidirectional integrator, at most this much of the hit buffer.
|
|
31
|
+
const LIGHT_CACHE_BYTES = 256 * 1024 * 1024;
|
|
32
|
+
const INFINITE_LIGHT_PATH_SHARE = 0.05;
|
|
33
|
+
// Light paths cost more than camera paths and help fewer pixels: at equal time half a pixel's worth beat one.
|
|
34
|
+
const LIGHT_PATHS_PER_PIXEL = 0.5;
|
|
35
|
+
const LIGHT_TAG_MAX = ( 1 << 24 ) - 1;
|
|
36
|
+
// Vertex merging: the radius at the first sample, in pixels' footprint where it gathers, shrinking as
|
|
37
|
+
// n^((α − 1) / 2) so the estimate converges (Knaus & Zwicker 2011).
|
|
38
|
+
const MERGE_RADIUS_PIXELS = 1;
|
|
39
|
+
const MERGE_ALPHA = 0.75;
|
|
40
|
+
const MERGE_TRUST = 0.25;
|
|
41
|
+
|
|
42
|
+
export class BidirectionalIntegrator {
|
|
43
|
+
|
|
44
|
+
/** @param {import('../Stages/PathTracer.js').PathTracer} pathTracer */
|
|
45
|
+
constructor( pathTracer ) {
|
|
46
|
+
|
|
47
|
+
this.pt = pathTracer;
|
|
48
|
+
this.merging = false;
|
|
49
|
+
this._mergeRadiusPixels = MERGE_RADIUS_PIXELS;
|
|
50
|
+
this._mergeHeadAttr = null;
|
|
51
|
+
this._lightCacheSlots = 0;
|
|
52
|
+
this._splatAttr = null;
|
|
53
|
+
this._lightTag = 0;
|
|
54
|
+
this._passTag = 0;
|
|
55
|
+
this._lightTraced = false;
|
|
56
|
+
this._lastLightBounceCounts = null;
|
|
57
|
+
this._lastLightBounceEnergy = null;
|
|
58
|
+
this._lastLightCurveKey = null;
|
|
59
|
+
this._lightCurveKey = null;
|
|
60
|
+
this.uniforms = {
|
|
61
|
+
lib: SHADING_LIB,
|
|
62
|
+
lightPaths: uniform( 0, 'uint' ),
|
|
63
|
+
slotsPerPath: uniform( 1, 'uint' ),
|
|
64
|
+
lightTag: uniform( 1, 'uint' ),
|
|
65
|
+
passTag: uniform( 0, 'uint' ),
|
|
66
|
+
lightTrace: uniform( 0, 'uint' ),
|
|
67
|
+
strategyView: uniform( 0, 'uint' ),
|
|
68
|
+
pixelArea: uniform( 1, 'float' ),
|
|
69
|
+
cameraPosition: uniform( new Vector3(), 'vec3' ),
|
|
70
|
+
cameraForward: uniform( new Vector3( 0, 0, - 1 ), 'vec3' ),
|
|
71
|
+
sunPick: uniform( 0, 'float' ),
|
|
72
|
+
emitterPick: uniform( 0, 'float' ),
|
|
73
|
+
envPick: uniform( 0, 'float' ),
|
|
74
|
+
// 1 while the CDF texture carries the environment's exact table.
|
|
75
|
+
envTable: uniform( 0, 'uint' ),
|
|
76
|
+
// Running sum over SOURCE, then each lamp list at sourceOffsets[ type ], sized to the lists' capacity.
|
|
77
|
+
sourceCdf: uniformArray( new Float32Array( SOURCE.LAMPS ), 'float' ),
|
|
78
|
+
sourceCount: SOURCE.LAMPS,
|
|
79
|
+
sourceOffsets: [ SOURCE.LAMPS, SOURCE.LAMPS, SOURCE.LAMPS, SOURCE.LAMPS ],
|
|
80
|
+
sceneCenter: uniform( new Vector3(), 'vec3' ),
|
|
81
|
+
sceneRadius: uniform( 1, 'float' ),
|
|
82
|
+
// The light guide (TSL/LightGuide.js): 1 once a table has been built since the last reset; and
|
|
83
|
+
// whether camera paths count their escapes.
|
|
84
|
+
guide: uniform( 0, 'uint' ),
|
|
85
|
+
guideLearning: uniform( 1, 'uint' ),
|
|
86
|
+
guideTexture: null,
|
|
87
|
+
// Vertex merging (integrator 'vcm', MergeKernel.js), fixed at build: the radius at a point is
|
|
88
|
+
// mergeConst + mergeSlope · its distance from the camera, at least mergeMin (Bidirectional.js mergeRadiusAt).
|
|
89
|
+
merging: false,
|
|
90
|
+
mergeConst: uniform( 0, 'float' ),
|
|
91
|
+
mergeSlope: uniform( 0, 'float' ),
|
|
92
|
+
mergeMin: uniform( 1, 'float' ),
|
|
93
|
+
// How far the weights take a merge's density at its word (Bidirectional.js mergeVmAt).
|
|
94
|
+
mergeTrust: uniform( MERGE_TRUST, 'float' ),
|
|
95
|
+
hashMask: uniform( 0, 'uint' ),
|
|
96
|
+
};
|
|
97
|
+
this._lightGuiding = true;
|
|
98
|
+
this._guideTexture = null;
|
|
99
|
+
this._guideBuildAttr = null;
|
|
100
|
+
this._u32Views = new WeakMap();
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 'bidirectional', or 'vcm' with vertex merging. */
|
|
105
|
+
get name() {
|
|
106
|
+
|
|
107
|
+
return this.merging ? 'vcm' : 'bidirectional';
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Called by PathTracer.setIntegrator() with the name chosen; the kernels rebuild after. */
|
|
112
|
+
select( name ) {
|
|
113
|
+
|
|
114
|
+
this.merging = name === 'vcm';
|
|
115
|
+
this.uniforms.merging = this.merging;
|
|
116
|
+
this._lastLightCurveKey = null;
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Integrator hooks: PathTracer calls these, in this order of a frame's life ──
|
|
121
|
+
|
|
122
|
+
/** The hit buffer's stride a path needs: its pending connection rides in four more slots. */
|
|
123
|
+
get hitStride() {
|
|
124
|
+
|
|
125
|
+
return HIT_STRIDE_BIDIRECTIONAL;
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Bytes of the hit buffer the light vertex cache takes beside the paths; sets the slot count. */
|
|
130
|
+
cacheBytes( maxBinding, deviceMemBytes ) {
|
|
131
|
+
|
|
132
|
+
const lightVertexBytes = LIGHT_VERTEX_STRIDE * 16;
|
|
133
|
+
const bytes = Math.floor( Math.min( LIGHT_CACHE_BYTES, maxBinding * 0.45, deviceMemBytes * 0.05 ) / lightVertexBytes ) * lightVertexBytes;
|
|
134
|
+
this._lightCacheSlots = bytes / lightVertexBytes;
|
|
135
|
+
return bytes;
|
|
136
|
+
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Light vertex cache slots, at the hit buffer's tail. */
|
|
140
|
+
get lightVertices() {
|
|
141
|
+
|
|
142
|
+
return this._lightCacheSlots;
|
|
143
|
+
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Before the kernels build: their source table's layout is baked in. */
|
|
147
|
+
beforeKernelBuild() {
|
|
148
|
+
|
|
149
|
+
this._sizeSourceTable();
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Its own buffers, for a frame of at most `maxPixels`. */
|
|
154
|
+
allocate( maxPixels ) {
|
|
155
|
+
|
|
156
|
+
// Light tracing's image: a u32 per channel per pixel, fixed point (LightSplatKernel).
|
|
157
|
+
if ( ! this._splatAttr || this._splatAttr.count < maxPixels * 3 ) {
|
|
158
|
+
|
|
159
|
+
freeStorageAttribute( this.pt.renderer, this._splatAttr );
|
|
160
|
+
this._splatAttr = gpuOnlyStorageAttribute( maxPixels * 3, 1, Uint32Array );
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Vertex merging's list heads: at least one a light vertex slot, a power of two (the hash's mask).
|
|
165
|
+
const hashSize = this.merging ? 2 ** Math.ceil( Math.log2( Math.max( this._lightCacheSlots, 1024 ) ) ) : 0;
|
|
166
|
+
if ( this._mergeHeadAttr?.count !== hashSize ) {
|
|
167
|
+
|
|
168
|
+
freeStorageAttribute( this.pt.renderer, this._mergeHeadAttr );
|
|
169
|
+
this._mergeHeadAttr = hashSize > 0 ? gpuOnlyStorageAttribute( hashSize, 1, Uint32Array ) : null;
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.uniforms.hashMask.value = Math.max( hashSize - 1, 0 );
|
|
174
|
+
|
|
175
|
+
// The light guide's table: built into a buffer, copied into a texture Shade can read (it has no buffer to spare).
|
|
176
|
+
if ( ! this._guideTexture ) {
|
|
177
|
+
|
|
178
|
+
this._guideTexture = new DataTexture( null, GUIDE_TEXTURE_WIDTH, GUIDE_BINS, RedFormat, FloatType );
|
|
179
|
+
this._guideTexture.source.dataReady = false;
|
|
180
|
+
this._guideTexture.needsUpdate = true;
|
|
181
|
+
this._guideBuildAttr = gpuOnlyStorageAttribute( GUIDE_ROW_STRIDE * GUIDE_BINS, 1, Float32Array );
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.uniforms.guideTexture = texture( this._guideTexture );
|
|
186
|
+
this.uniforms.guide.value = 0;
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Its kernels, beside the path tracer's. `ctx` carries what _buildWavefrontKernels built: the kernel manager and
|
|
192
|
+
* the buffers, the scene nodes (fresh*) and `own`, which hands a kernel this renderer's scene resources.
|
|
193
|
+
*/
|
|
194
|
+
registerKernels( ctx ) {
|
|
195
|
+
|
|
196
|
+
const {
|
|
197
|
+
km, qm, pb, counters, bounceCountsBuf, wfCurrentBounce, copyReadB, copyWriteA, maxRays, w, own,
|
|
198
|
+
freshLight, freshTri, freshBvh, freshMat, freshEnvTex, freshEnvCDF,
|
|
199
|
+
} = ctx;
|
|
200
|
+
|
|
201
|
+
const bd = this.uniforms;
|
|
202
|
+
const n = qm.MAX_BOUNCE_SNAPSHOTS;
|
|
203
|
+
|
|
204
|
+
// The light pass keeps its own survivor curve, after the camera's: [2n, 3n) counts, [3n, 4n) energy.
|
|
205
|
+
const recordLightCurve = ( active ) => {
|
|
206
|
+
|
|
207
|
+
const slot = uint( wfCurrentBounce ).clamp( uint( 0 ), uint( n - 1 ) );
|
|
208
|
+
bounceCountsBuf.element( slot.add( uint( 2 * n ) ) ).assign( active );
|
|
209
|
+
bounceCountsBuf.element( slot.add( uint( 3 * n ) ) ).assign( atomicLoad( counters.element( uint( COUNTER.ACTIVE_ENERGY ) ) ) );
|
|
210
|
+
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), active );
|
|
211
|
+
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
km.register( 'lightSnapshot', Fn( () => {
|
|
215
|
+
|
|
216
|
+
recordLightCurve( atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) ) );
|
|
217
|
+
|
|
218
|
+
} )().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] ) );
|
|
219
|
+
|
|
220
|
+
km.register( 'lightCopyback', Fn( () => {
|
|
221
|
+
|
|
222
|
+
const tid = instanceIndex;
|
|
223
|
+
const active = atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) );
|
|
224
|
+
If( tid.equal( uint( 0 ) ), () => {
|
|
225
|
+
|
|
226
|
+
recordLightCurve( active );
|
|
227
|
+
|
|
228
|
+
} );
|
|
229
|
+
If( tid.greaterThanEqual( active ), () => {
|
|
230
|
+
|
|
231
|
+
Return();
|
|
232
|
+
|
|
233
|
+
} );
|
|
234
|
+
copyWriteA.element( tid ).assign( copyReadB.element( tid ) );
|
|
235
|
+
|
|
236
|
+
} )().compute( [ Math.ceil( maxRays / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] ) );
|
|
237
|
+
|
|
238
|
+
km.register( 'guideClear', buildGuideClearKernel( { counters } ) );
|
|
239
|
+
km.register( 'guideBuild', buildGuideKernel( { counters, out: storage( this._guideBuildAttr, 'float' ) } ) );
|
|
240
|
+
|
|
241
|
+
km.register( 'lightGenerate', own( buildLightGenerateKernel( {
|
|
242
|
+
rayBufferRW: pb.rayBuffer.rw,
|
|
243
|
+
hitBufferRW: pb.hitBuffer.rw,
|
|
244
|
+
activeIndicesRW: qm.activeIndices.a,
|
|
245
|
+
counters,
|
|
246
|
+
lightBuffer: freshLight,
|
|
247
|
+
triangleBuffer: freshTri,
|
|
248
|
+
bvhBuffer: freshBvh,
|
|
249
|
+
emissiveTriangleCount: this.pt.emissiveTriangleCount,
|
|
250
|
+
emissiveVec4Offset: this.pt.emissiveVec4Offset,
|
|
251
|
+
emissiveTotalPower: this.pt.emissiveTotalPower,
|
|
252
|
+
emissiveBoost: this.pt.emissiveBoost,
|
|
253
|
+
sunDirection: this.pt.sunDirection,
|
|
254
|
+
sunRadiance: this.pt.sunRadiance,
|
|
255
|
+
sunParams: this.pt.sunParams,
|
|
256
|
+
environmentIntensity: this.pt.environmentIntensity,
|
|
257
|
+
envTexture: freshEnvTex,
|
|
258
|
+
envCDFTexture: freshEnvCDF,
|
|
259
|
+
envMatrix: this.pt.environmentMatrix,
|
|
260
|
+
envResolution: this.pt.envResolution,
|
|
261
|
+
directionalLightsBuffer: this.pt.directionalLightsBufferNode,
|
|
262
|
+
areaLightsBuffer: this.pt.areaLightsBufferNode,
|
|
263
|
+
pointLightsBuffer: this.pt.pointLightsBufferNode,
|
|
264
|
+
spotLightsBuffer: this.pt.spotLightsBufferNode,
|
|
265
|
+
bidirectional: bd,
|
|
266
|
+
resolution: this.pt.resolution,
|
|
267
|
+
frame: this.pt.seedFrame,
|
|
268
|
+
transmissiveBounces: this.pt.transmissiveBounces,
|
|
269
|
+
} )() ).compute( [ Math.ceil( maxRays / LIGHT_GENERATE_WG_SIZE ), 1, 1 ], [ LIGHT_GENERATE_WG_SIZE, 1, 1 ] ) );
|
|
270
|
+
|
|
271
|
+
km.register( 'connect', own( buildConnectKernel( {
|
|
272
|
+
rayBufferRW: pb.rayBuffer.rw,
|
|
273
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
274
|
+
activeIndicesRO: qm.getActiveReadRO(),
|
|
275
|
+
counters,
|
|
276
|
+
bvhBuffer: freshBvh,
|
|
277
|
+
triangleBuffer: freshTri,
|
|
278
|
+
materialBuffer: freshMat,
|
|
279
|
+
resolution: this.pt.resolution,
|
|
280
|
+
frame: this.pt.seedFrame,
|
|
281
|
+
accumFrame: this.pt.frame,
|
|
282
|
+
currentBounce: this.pt._wfCurrentBounce,
|
|
283
|
+
chunkRowBase: this.pt._wfChunkRowBase,
|
|
284
|
+
lightPaths: bd.lightPaths,
|
|
285
|
+
slotsPerPath: bd.slotsPerPath,
|
|
286
|
+
lightTag: bd.lightTag,
|
|
287
|
+
passTag: bd.passTag,
|
|
288
|
+
strategyView: bd.strategyView,
|
|
289
|
+
maxBounceCount: this.pt.maxBounces,
|
|
290
|
+
globalIlluminationIntensity: this.pt.globalIlluminationIntensity,
|
|
291
|
+
fireflyThreshold: this.pt.fireflyThreshold,
|
|
292
|
+
mergeVm: bd.merging ? ( p ) => mergeVmAt( bd, p ) : null,
|
|
293
|
+
} )() ).compute( [ Math.ceil( maxRays / CONNECT_WG_SIZE ), 1, 1 ], [ CONNECT_WG_SIZE, 1, 1 ] ) );
|
|
294
|
+
|
|
295
|
+
if ( bd.merging ) {
|
|
296
|
+
|
|
297
|
+
const head = storage( this._mergeHeadAttr, 'uint' ).toAtomic();
|
|
298
|
+
km.register( 'mergeClear', buildMergeClearKernel( { head, hashSize: this._mergeHeadAttr.count } ) );
|
|
299
|
+
km.register( 'mergeInsert', buildMergeInsertKernel( {
|
|
300
|
+
hitBufferRW: pb.hitBuffer.rw,
|
|
301
|
+
head,
|
|
302
|
+
bidirectional: bd,
|
|
303
|
+
} )().compute( [ Math.ceil( this._lightCacheSlots / MERGE_WG_SIZE ), 1, 1 ], [ MERGE_WG_SIZE, 1, 1 ] ) );
|
|
304
|
+
km.register( 'merge', own( buildMergeKernel( {
|
|
305
|
+
rayBufferRW: pb.rayBuffer.rw,
|
|
306
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
307
|
+
activeIndicesRO: qm.getActiveReadRO(),
|
|
308
|
+
counters,
|
|
309
|
+
head,
|
|
310
|
+
materialBuffer: freshMat,
|
|
311
|
+
bidirectional: bd,
|
|
312
|
+
maxBounceCount: this.pt.maxBounces,
|
|
313
|
+
globalIlluminationIntensity: this.pt.globalIlluminationIntensity,
|
|
314
|
+
fireflyThreshold: this.pt.fireflyThreshold,
|
|
315
|
+
accumFrame: this.pt.frame,
|
|
316
|
+
} )() ).compute( [ Math.ceil( maxRays / MERGE_WG_SIZE ), 1, 1 ], [ MERGE_WG_SIZE, 1, 1 ] ) );
|
|
317
|
+
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const splatBuffer = storage( this._splatAttr, 'uint' ).toAtomic();
|
|
321
|
+
|
|
322
|
+
km.register( 'lightSplat', own( buildLightSplatKernel( {
|
|
323
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
324
|
+
splatBuffer,
|
|
325
|
+
bvhBuffer: freshBvh,
|
|
326
|
+
triangleBuffer: freshTri,
|
|
327
|
+
materialBuffer: freshMat,
|
|
328
|
+
lightPaths: bd.lightPaths,
|
|
329
|
+
slotsPerPath: bd.slotsPerPath,
|
|
330
|
+
lightTag: bd.lightTag,
|
|
331
|
+
strategyView: bd.strategyView,
|
|
332
|
+
cameraPosition: bd.cameraPosition,
|
|
333
|
+
cameraForward: bd.cameraForward,
|
|
334
|
+
cameraViewMatrix: this.pt.cameraViewMatrix,
|
|
335
|
+
cameraProjectionMatrix: this.pt.cameraProjectionMatrix,
|
|
336
|
+
pixelArea: bd.pixelArea,
|
|
337
|
+
renderWidth: this.pt._wfRenderWidth,
|
|
338
|
+
renderHeight: this.pt._wfRenderHeight,
|
|
339
|
+
globalIlluminationIntensity: this.pt.globalIlluminationIntensity,
|
|
340
|
+
fireflyThreshold: this.pt.fireflyThreshold,
|
|
341
|
+
accumFrame: this.pt.frame,
|
|
342
|
+
frame: this.pt.seedFrame,
|
|
343
|
+
mergeVm: bd.merging ? ( p ) => mergeVmAt( bd, p ) : null,
|
|
344
|
+
} )() ).compute( [ Math.ceil( this._lightCacheSlots / LIGHT_SPLAT_WG_SIZE ), 1, 1 ], [ LIGHT_SPLAT_WG_SIZE, 1, 1 ] ) );
|
|
345
|
+
|
|
346
|
+
km.register( 'splatResolve', buildSplatResolveKernel( {
|
|
347
|
+
rayBufferRW: pb.rayBuffer.rw,
|
|
348
|
+
splatBuffer,
|
|
349
|
+
renderWidth: this.pt._wfRenderWidth,
|
|
350
|
+
chunkRowBase: this.pt._wfChunkRowBase,
|
|
351
|
+
chunkRows: this.pt._wfChunkRows,
|
|
352
|
+
} )().compute(
|
|
353
|
+
[ Math.ceil( w / SPLAT_RESOLVE_WG_SIZE ), Math.ceil( this.pt._chunkRows / SPLAT_RESOLVE_WG_SIZE ), 1 ],
|
|
354
|
+
[ SPLAT_RESOLVE_WG_SIZE, SPLAT_RESOLVE_WG_SIZE, 1 ]
|
|
355
|
+
) );
|
|
356
|
+
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Start of a frame: the source table, the light guide, then the light subpaths. False when nothing can start a
|
|
361
|
+
* light path, and the frame runs as the path tracer's own.
|
|
362
|
+
*/
|
|
363
|
+
beginFrame( loopBound ) {
|
|
364
|
+
|
|
365
|
+
const active = this._updateBidirectionalUniforms();
|
|
366
|
+
this._lightTraced = active && this.uniforms.lightTrace.value > 0;
|
|
367
|
+
if ( active ) {
|
|
368
|
+
|
|
369
|
+
this._updateLightGuide();
|
|
370
|
+
this._traceLightPaths( loopBound );
|
|
371
|
+
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return active;
|
|
375
|
+
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Before each Shade of an active frame. */
|
|
379
|
+
beforeShade() {
|
|
380
|
+
|
|
381
|
+
this.uniforms.passTag.value = this._nextPassTag();
|
|
382
|
+
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** After each Shade of an active frame. */
|
|
386
|
+
afterShade( km ) {
|
|
387
|
+
|
|
388
|
+
km.dispatch( 'connect' );
|
|
389
|
+
if ( this.merging ) km.dispatch( 'merge' );
|
|
390
|
+
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Before FinalWrite, each chunk of an active frame. */
|
|
394
|
+
resolve( km ) {
|
|
395
|
+
|
|
396
|
+
if ( this._lightTraced ) km.dispatch( 'splatResolve' );
|
|
397
|
+
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** What the light pass's survivor curve was measured for; the readback keeps it with the curve. */
|
|
401
|
+
get curveKey() {
|
|
402
|
+
|
|
403
|
+
return this._lightCurveKey;
|
|
404
|
+
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The light pass's survivor curve, read back with the camera's. */
|
|
408
|
+
applyLightCurve( counts, energy, key ) {
|
|
409
|
+
|
|
410
|
+
this._lastLightBounceCounts = counts;
|
|
411
|
+
this._lastLightBounceEnergy = energy;
|
|
412
|
+
this._lastLightCurveKey = key;
|
|
413
|
+
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** For the build's log line. */
|
|
417
|
+
describe() {
|
|
418
|
+
|
|
419
|
+
return `${this.name}: ${this._lightCacheSlots.toLocaleString( 'en-US' )} light vertex slots`;
|
|
420
|
+
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** GPU memory of its own, for the VRAM tracker. */
|
|
424
|
+
gpuResources() {
|
|
425
|
+
|
|
426
|
+
return this._splatAttr ? [ this._splatAttr, this._mergeHeadAttr ].filter( Boolean ) : null;
|
|
427
|
+
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Frees its buffers; the path tracer calls it when another integrator is chosen, and on dispose. */
|
|
431
|
+
dispose() {
|
|
432
|
+
|
|
433
|
+
freeStorageAttribute( this.pt.renderer, this._splatAttr );
|
|
434
|
+
this._splatAttr = null;
|
|
435
|
+
freeStorageAttribute( this.pt.renderer, this._mergeHeadAttr );
|
|
436
|
+
this._mergeHeadAttr = null;
|
|
437
|
+
this._disposeLightGuide();
|
|
438
|
+
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Controls ──
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Verification only: keep one bidirectional strategy, MIS-weighted, or alone at full weight.
|
|
445
|
+
* @param {'all'|'hit'|'nee'|'connect'|'lightTrace'|'merge'} strategy
|
|
446
|
+
*/
|
|
447
|
+
setBidirectionalStrategy( strategy = 'all', { alone = false } = {} ) {
|
|
448
|
+
|
|
449
|
+
const code = { all: STRATEGY.ALL, hit: STRATEGY.HIT, nee: STRATEGY.NEE, connect: STRATEGY.CONNECT, lightTrace: STRATEGY.LIGHT_TRACE, merge: STRATEGY.MERGE }[ strategy ] ?? STRATEGY.ALL;
|
|
450
|
+
this.uniforms.strategyView.value = code === STRATEGY.ALL ? code : code + ( alone ? STRATEGY_ALONE : 0 );
|
|
451
|
+
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Vertex merging's radius at the first sample, in pixels: the footprint where it gathers, so the blur is the same
|
|
456
|
+
* on screen at any scene scale. It shrinks with every sample after. Larger gathers more light vertices — less
|
|
457
|
+
* noise in caustics, more blur — at more cost.
|
|
458
|
+
*/
|
|
459
|
+
setMergeRadius( pixels = MERGE_RADIUS_PIXELS ) {
|
|
460
|
+
|
|
461
|
+
this._mergeRadiusPixels = Math.max( pixels, 1e-3 );
|
|
462
|
+
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* How much of the light other strategies can also reach vertex merging takes: 1 weighs merges by their density,
|
|
467
|
+
* less hands that light back to the unblurred strategies. Light only merging reaches is all merging's either way.
|
|
468
|
+
*/
|
|
469
|
+
setMergeTrust( trust = MERGE_TRUST ) {
|
|
470
|
+
|
|
471
|
+
this.uniforms.mergeTrust.value = Math.max( trust, 1e-4 );
|
|
472
|
+
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// False when nothing can start a light path.
|
|
476
|
+
_updateBidirectionalUniforms() {
|
|
477
|
+
|
|
478
|
+
const bd = this.uniforms;
|
|
479
|
+
const w = this.pt._wfRenderWidth.value;
|
|
480
|
+
const h = this.pt._wfRenderHeight.value;
|
|
481
|
+
const slots = this.pt.maxBounces.value + 1;
|
|
482
|
+
const total = this._updateSourceTable();
|
|
483
|
+
const paths = total > 0
|
|
484
|
+
? Math.min( Math.ceil( w * h * LIGHT_PATHS_PER_PIXEL ), this.pt._packedBuffers.capacity, Math.floor( this._lightCacheSlots / slots ) )
|
|
485
|
+
: 0;
|
|
486
|
+
|
|
487
|
+
bd.lightPaths.value = paths;
|
|
488
|
+
bd.slotsPerPath.value = slots;
|
|
489
|
+
this._lightCurveKey = `${paths}:${slots}:${this.pt._bounceLoopBound()}`;
|
|
490
|
+
if ( paths === 0 ) {
|
|
491
|
+
|
|
492
|
+
bd.lightTrace.value = 0;
|
|
493
|
+
return false;
|
|
494
|
+
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
this._lightTag = this._lightTag % LIGHT_TAG_MAX + 1;
|
|
498
|
+
bd.lightTag.value = this._lightTag;
|
|
499
|
+
|
|
500
|
+
const world = this.pt.cameraWorldMatrix.value.elements;
|
|
501
|
+
bd.cameraPosition.value.set( world[ 12 ], world[ 13 ], world[ 14 ] );
|
|
502
|
+
bd.cameraForward.value.set( - world[ 8 ], - world[ 9 ], - world[ 10 ] ).normalize();
|
|
503
|
+
const projection = this.pt.cameraProjectionMatrix.value.elements;
|
|
504
|
+
bd.pixelArea.value = 4 / ( projection[ 0 ] * projection[ 5 ] * w * h );
|
|
505
|
+
// Light tracing needs a pinhole camera.
|
|
506
|
+
const pinhole = this.pt.cameraProjection.value === CAMERA_PROJECTION_IDS.perspective && ! this.pt.enableDOF.value;
|
|
507
|
+
bd.lightTrace.value = pinhole ? 1 : 0;
|
|
508
|
+
if ( this.merging ) this._updateMergeRadius( w, h );
|
|
509
|
+
return true;
|
|
510
|
+
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// A pixel's footprint: grows with the distance from a perspective or panoramic camera, constant for an orthographic one.
|
|
514
|
+
_updateMergeRadius( w, h ) {
|
|
515
|
+
|
|
516
|
+
const bd = this.uniforms;
|
|
517
|
+
const pixels = this._mergeRadiusPixels * Math.pow( this.pt.frameCount + 1, ( MERGE_ALPHA - 1 ) / 2 );
|
|
518
|
+
const projection = this.pt.cameraProjection.value;
|
|
519
|
+
const p = this.pt.cameraProjectionMatrix.value.elements;
|
|
520
|
+
let slope = 0, constant = 0;
|
|
521
|
+
if ( projection === CAMERA_PROJECTION_IDS.orthographic ) constant = pixels * 2 / ( p[ 5 ] * h );
|
|
522
|
+
else if ( projection === CAMERA_PROJECTION_IDS.equirectangular ) slope = pixels * 2 * Math.PI / w;
|
|
523
|
+
else slope = pixels * Math.sqrt( bd.pixelArea.value );
|
|
524
|
+
bd.mergeSlope.value = Math.min( slope, 0.1 );
|
|
525
|
+
bd.mergeConst.value = constant;
|
|
526
|
+
// Nearer than a thousandth of the scene, the radius stops shrinking: shells start there.
|
|
527
|
+
bd.mergeMin.value = Math.max( constant, slope * 1e-3 * bd.sceneRadius.value, 1e-9 );
|
|
528
|
+
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Sizes the source table to the lamp lists' capacity; its layout is baked into the kernels. */
|
|
532
|
+
_sizeSourceTable() {
|
|
533
|
+
|
|
534
|
+
const bd = this.uniforms;
|
|
535
|
+
let at = SOURCE.LAMPS;
|
|
536
|
+
[ 'directional', 'area', 'point', 'spot' ].forEach( ( type, i ) => {
|
|
537
|
+
|
|
538
|
+
bd.sourceOffsets[ i ] = at;
|
|
539
|
+
at += this.pt[ `${type}LightsBufferNode` ].array.length / LIGHT_FLOATS[ type ];
|
|
540
|
+
|
|
541
|
+
} );
|
|
542
|
+
bd.sourceCount = at;
|
|
543
|
+
if ( bd.sourceCdf.array.length !== at ) bd.sourceCdf.array = new Float32Array( at );
|
|
544
|
+
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Each source's chance of starting a light path, by the luminous flux it sends into the scene.
|
|
549
|
+
* @returns {number} the total flux, 0 when nothing emits
|
|
550
|
+
*/
|
|
551
|
+
_updateSourceTable() {
|
|
552
|
+
|
|
553
|
+
const bd = this.uniforms;
|
|
554
|
+
const cdf = bd.sourceCdf.array;
|
|
555
|
+
const flux = new Float64Array( cdf.length );
|
|
556
|
+
const lum = ( a, i ) => Math.max( 0.2126 * a[ i ] + 0.7152 * a[ i + 1 ] + 0.0722 * a[ i + 2 ], 0 );
|
|
557
|
+
const lights = ( type ) => ( { a: this.pt[ `${type}LightsBufferNode` ].array, n: this.pt[ `num${type[ 0 ].toUpperCase()}${type.slice( 1 )}Lights` ].value, f: LIGHT_FLOATS[ type ] } );
|
|
558
|
+
const directional = lights( 'directional' );
|
|
559
|
+
|
|
560
|
+
const environmentOn = this.pt.enableEnvironment.value > 0;
|
|
561
|
+
const table = this.pt.environment.exactTable;
|
|
562
|
+
bd.envTable.value = table ? 1 : 0;
|
|
563
|
+
const atInfinity = environmentOn && ( this.pt.hasSun.value > 0 || table ) || directional.n > 0;
|
|
564
|
+
// What a light at infinity sends through the scene's disc mostly lands where its NEE does better: next to
|
|
565
|
+
// lamps or emitters it gets this share of its flux in light paths (alone, it gets them all regardless).
|
|
566
|
+
// Vertex merging sizes its radius by the scene, so it measures the disc too.
|
|
567
|
+
const disc = atInfinity || this.merging ? this._sceneDisc() : 0;
|
|
568
|
+
const discArea = atInfinity ? disc * INFINITE_LIGHT_PATH_SHARE : 0;
|
|
569
|
+
if ( environmentOn && table ) flux[ SOURCE.ENVIRONMENT ] = Math.max( table.radianceIntegral, 0 ) * this.pt.environmentIntensity.value * discArea;
|
|
570
|
+
|
|
571
|
+
if ( this.pt.emissiveTriangleCount.value > 0 ) flux[ SOURCE.EMITTERS ] = Math.PI * this.pt.emissiveBoost.value * this.pt.emissiveTotalPower.value;
|
|
572
|
+
if ( environmentOn && this.pt.hasSun.value > 0 ) {
|
|
573
|
+
|
|
574
|
+
const { x: r, y: g, z: b } = this.pt.sunRadiance.value;
|
|
575
|
+
flux[ SOURCE.SUN ] = Math.max( 0.2126 * r + 0.7152 * g + 0.0722 * b, 0 ) * this.pt.environmentIntensity.value * this.pt.sunParams.value.y * discArea;
|
|
576
|
+
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
for ( let i = 0; i < directional.n; i ++ ) flux[ bd.sourceOffsets[ 0 ] + i ] = lum( directional.a, i * directional.f + 3 ) * directional.a[ i * directional.f + 6 ] * discArea;
|
|
580
|
+
|
|
581
|
+
const area = lights( 'area' );
|
|
582
|
+
for ( let i = 0; i < area.n; i ++ ) {
|
|
583
|
+
|
|
584
|
+
const a = area.a, o = i * area.f;
|
|
585
|
+
const cx = a[ o + 4 ] * a[ o + 8 ] - a[ o + 5 ] * a[ o + 7 ], cy = a[ o + 5 ] * a[ o + 6 ] - a[ o + 3 ] * a[ o + 8 ], cz = a[ o + 3 ] * a[ o + 7 ] - a[ o + 4 ] * a[ o + 6 ];
|
|
586
|
+
const size = 4 * Math.hypot( cx, cy, cz ) * ( a[ o + 15 ] > 0.5 ? Math.PI / 4 : 1 );
|
|
587
|
+
flux[ bd.sourceOffsets[ 1 ] + i ] = size > 0 ? lum( a, o + 9 ) * Math.max( a[ o + 12 ], 0 ) * ( a[ o + 13 ] > 0.5 ? 1 : size ) : 0;
|
|
588
|
+
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const point = lights( 'point' );
|
|
592
|
+
for ( let i = 0; i < point.n; i ++ ) flux[ bd.sourceOffsets[ 2 ] + i ] = 4 * Math.PI * lum( point.a, i * point.f + 3 ) * Math.max( point.a[ i * point.f + 6 ], 0 );
|
|
593
|
+
|
|
594
|
+
const spot = lights( 'spot' );
|
|
595
|
+
for ( let i = 0; i < spot.n; i ++ ) {
|
|
596
|
+
|
|
597
|
+
const o = i * spot.f;
|
|
598
|
+
flux[ bd.sourceOffsets[ 3 ] + i ] = 2 * Math.PI * ( 1 - Math.cos( spot.a[ o + 10 ] ) ) * lum( spot.a, o + 6 ) * Math.max( spot.a[ o + 9 ], 0 );
|
|
599
|
+
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
let total = 0;
|
|
603
|
+
for ( let i = 0; i < flux.length; i ++ ) total += Number.isFinite( flux[ i ] ) ? flux[ i ] : 0;
|
|
604
|
+
let sum = 0;
|
|
605
|
+
for ( let i = 0; i < flux.length; i ++ ) {
|
|
606
|
+
|
|
607
|
+
sum += Number.isFinite( flux[ i ] ) ? flux[ i ] : 0;
|
|
608
|
+
cdf[ i ] = total > 0 ? sum / total : 0;
|
|
609
|
+
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if ( total > 0 ) cdf[ cdf.length - 1 ] = 1;
|
|
613
|
+
|
|
614
|
+
// The shaders difference the stored sum, so these are taken from it the same way.
|
|
615
|
+
const pick = ( i ) => Math.fround( cdf[ i ] - ( i > 0 ? cdf[ i - 1 ] : 0 ) );
|
|
616
|
+
bd.sunPick.value = pick( SOURCE.SUN );
|
|
617
|
+
bd.emitterPick.value = pick( SOURCE.EMITTERS );
|
|
618
|
+
bd.envPick.value = pick( SOURCE.ENVIRONMENT );
|
|
619
|
+
return total;
|
|
620
|
+
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// The disc a light at infinity starts its paths on: the visible scene's bounding sphere, facing it. Its area, or 0.
|
|
624
|
+
_sceneDisc() {
|
|
625
|
+
|
|
626
|
+
const bd = this.uniforms;
|
|
627
|
+
const bounds = this._visibleSceneBounds();
|
|
628
|
+
if ( ! bounds ) return 0;
|
|
629
|
+
|
|
630
|
+
const [ x0, y0, z0 ] = bounds.min;
|
|
631
|
+
const [ x1, y1, z1 ] = bounds.max;
|
|
632
|
+
const radius = 0.5 * Math.hypot( x1 - x0, y1 - y0, z1 - z0 );
|
|
633
|
+
if ( ! ( radius > 0 && Number.isFinite( radius ) ) ) return 0;
|
|
634
|
+
bd.sceneCenter.value.set( ( x0 + x1 ) / 2, ( y0 + y1 ) / 2, ( z0 + z1 ) / 2 );
|
|
635
|
+
bd.sceneRadius.value = radius;
|
|
636
|
+
return Math.PI * radius * radius;
|
|
637
|
+
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* The visible placements' world box, read from the TLAS as uploaded; hidden meshes are left out.
|
|
642
|
+
* @returns {?{min: number[], max: number[]}}
|
|
643
|
+
*/
|
|
644
|
+
_visibleSceneBounds() {
|
|
645
|
+
|
|
646
|
+
const records = this.pt._bvhRecords;
|
|
647
|
+
const flat = records ? null : this.pt.bvhStorageAttr?.array;
|
|
648
|
+
if ( ! records && ! ( flat?.length >= 16 ) ) return null;
|
|
649
|
+
|
|
650
|
+
const view = ( f ) => {
|
|
651
|
+
|
|
652
|
+
let u = this._u32Views.get( f );
|
|
653
|
+
if ( ! u ) this._u32Views.set( f, u = new Uint32Array( f.buffer, f.byteOffset, f.length ) );
|
|
654
|
+
return u;
|
|
655
|
+
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const node = ( i ) => {
|
|
659
|
+
|
|
660
|
+
const f = records ? records.chunkFor( i ) : flat;
|
|
661
|
+
return { f, u: view( f ), o: records ? records.baseOf( i ) : i * 16 };
|
|
662
|
+
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const root = node( 0 );
|
|
666
|
+
if ( root.u[ root.o + 3 ] >= BVH_MAX_INDEX ) return null;
|
|
667
|
+
// A tree that reaches a node twice is malformed; no search visits more nodes than the tree holds.
|
|
668
|
+
const nodeCount = records ? records.recordCount : flat.length / 16;
|
|
669
|
+
|
|
670
|
+
const min = [], max = [];
|
|
671
|
+
for ( let axis = 0; axis < 3; axis ++ ) for ( const sign of [ 1, - 1 ] ) {
|
|
672
|
+
|
|
673
|
+
// Inner node: [leftMin, left, leftMax, right, rightMin, -, rightMax, -]; a child no further out than the best visible leaf is skipped.
|
|
674
|
+
const score = ( n, child ) => sign > 0 ? n.f[ n.o + child * 8 + 4 + axis ] : - n.f[ n.o + child * 8 + axis ];
|
|
675
|
+
const childIndex = ( n, child ) => n.u[ n.o + 3 + child * 4 ];
|
|
676
|
+
let best = - Infinity;
|
|
677
|
+
const stack = [];
|
|
678
|
+
const push = ( n ) => {
|
|
679
|
+
|
|
680
|
+
const a = score( n, 0 ), b = score( n, 1 );
|
|
681
|
+
const first = a <= b ? 0 : 1;
|
|
682
|
+
stack.push( childIndex( n, first ), first === 0 ? a : b, childIndex( n, 1 - first ), first === 0 ? b : a );
|
|
683
|
+
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
push( root );
|
|
687
|
+
let visits = 0;
|
|
688
|
+
while ( stack.length ) {
|
|
689
|
+
|
|
690
|
+
const s = stack.pop(), index = stack.pop();
|
|
691
|
+
if ( s <= best ) continue;
|
|
692
|
+
if ( index >= nodeCount || ++ visits > nodeCount ) return null;
|
|
693
|
+
const n = node( index );
|
|
694
|
+
if ( n.u[ n.o + 3 ] >= BVH_MAX_INDEX ) {
|
|
695
|
+
|
|
696
|
+
if ( n.f[ n.o + 2 ] !== 0 ) best = s;
|
|
697
|
+
|
|
698
|
+
} else push( n );
|
|
699
|
+
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if ( best === - Infinity ) return null;
|
|
703
|
+
( sign > 0 ? max : min )[ axis ] = sign > 0 ? best : - best;
|
|
704
|
+
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return { min, max };
|
|
708
|
+
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Lights at infinity start their light paths where camera paths escaped (TSL/LightGuide.js). Each reset forgets
|
|
713
|
+
* the counts; the table is rebuilt from them at frames 1, 2, 4 … 32 and every 32nd after, so a render of the same
|
|
714
|
+
* input learns the same table. Off: light paths start uniformly over the scene's disc, as before.
|
|
715
|
+
* @param {boolean} enabled
|
|
716
|
+
*/
|
|
717
|
+
setLightGuiding( enabled ) {
|
|
718
|
+
|
|
719
|
+
this._lightGuiding = enabled !== false;
|
|
720
|
+
this.uniforms.guide.value = 0;
|
|
721
|
+
this.pt.reset();
|
|
722
|
+
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
_updateLightGuide() {
|
|
726
|
+
|
|
727
|
+
const bd = this.uniforms;
|
|
728
|
+
const km = this.pt._kernelManager;
|
|
729
|
+
bd.guideLearning.value = this._lightGuiding ? 1 : 0;
|
|
730
|
+
const f = this.pt.frameCount;
|
|
731
|
+
if ( f === 0 ) {
|
|
732
|
+
|
|
733
|
+
km.dispatch( 'guideClear' );
|
|
734
|
+
bd.guide.value = 0;
|
|
735
|
+
return;
|
|
736
|
+
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if ( ! this._lightGuiding || ! ( f <= 32 ? ( f & ( f - 1 ) ) === 0 : f % 32 === 0 ) ) return;
|
|
740
|
+
km.dispatch( 'guideBuild' );
|
|
741
|
+
|
|
742
|
+
const backend = this.pt.renderer.backend;
|
|
743
|
+
this.pt.renderer.initTexture( this._guideTexture );
|
|
744
|
+
const encoder = backend.device.createCommandEncoder( { label: 'LightGuide' } );
|
|
745
|
+
encoder.copyBufferToTexture(
|
|
746
|
+
{ buffer: backend.get( this._guideBuildAttr ).buffer, bytesPerRow: GUIDE_ROW_STRIDE * 4, rowsPerImage: GUIDE_BINS },
|
|
747
|
+
{ texture: backend.get( this._guideTexture ).texture },
|
|
748
|
+
[ GUIDE_TEXTURE_WIDTH, GUIDE_BINS ],
|
|
749
|
+
);
|
|
750
|
+
backend.device.queue.submit( [ encoder.finish() ] );
|
|
751
|
+
bd.guide.value = 1;
|
|
752
|
+
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
_disposeLightGuide() {
|
|
756
|
+
|
|
757
|
+
freeStorageAttribute( this.pt.renderer, this._guideBuildAttr );
|
|
758
|
+
this._guideTexture?.dispose();
|
|
759
|
+
this._guideTexture = this._guideBuildAttr = null;
|
|
760
|
+
this.uniforms.guideTexture = null;
|
|
761
|
+
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
_nextPassTag() {
|
|
765
|
+
|
|
766
|
+
this._passTag = ( this._passTag + 1 ) & 0x7FFFFFFF;
|
|
767
|
+
return ( PASS_TAG_BIT | this._passTag ) >>> 0;
|
|
768
|
+
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Sized and cut short off its own survivor curve, as the camera loop is.
|
|
772
|
+
_traceLightPaths( loopBound ) {
|
|
773
|
+
|
|
774
|
+
const km = this.pt._kernelManager;
|
|
775
|
+
const paths = this.uniforms.lightPaths.value;
|
|
776
|
+
|
|
777
|
+
this.pt._wfChunkRowBase.value = 0;
|
|
778
|
+
this.pt._wfMaxRayCount.value = paths;
|
|
779
|
+
km.setDispatchForCount( 'lightGenerate', paths );
|
|
780
|
+
km.dispatch( 'lightGenerate' );
|
|
781
|
+
|
|
782
|
+
const dynamic = this.pt._useDynamicDispatch;
|
|
783
|
+
const curveValid = this._lastLightCurveKey === this._lightCurveKey;
|
|
784
|
+
const counts = curveValid ? this._lastLightBounceCounts : null;
|
|
785
|
+
const energy = curveValid ? this._lastLightBounceEnergy : null;
|
|
786
|
+
const exitEnergy = this.pt._bounceEarlyExitThreshold >= 0 ? this.pt._bounceEarlyExitThreshold * paths * ENERGY_SCALE : - 1;
|
|
787
|
+
|
|
788
|
+
for ( let bounce = 0; bounce <= loopBound; bounce ++ ) {
|
|
789
|
+
|
|
790
|
+
this.pt._wfCurrentBounce.value = bounce;
|
|
791
|
+
|
|
792
|
+
const prev = bounce > 0 ? counts?.[ bounce - 1 ] : undefined;
|
|
793
|
+
const sized = dynamic && prev > 0 ? Math.min( paths, Math.ceil( prev * 1.5 ) + 1024 ) : paths;
|
|
794
|
+
for ( const k of BOUNCE_KERNELS ) km.setDispatchForCount( k, sized );
|
|
795
|
+
if ( ! dynamic ) km.dispatch( 'enterFull' );
|
|
796
|
+
|
|
797
|
+
km.dispatch( 'extend' );
|
|
798
|
+
if ( this.pt._sortMaterials ) {
|
|
799
|
+
|
|
800
|
+
km.dispatch( 'resetGlobalHist' );
|
|
801
|
+
km.dispatch( 'globalHist' );
|
|
802
|
+
km.dispatch( 'globalPrefix' );
|
|
803
|
+
km.dispatch( 'globalScatter' );
|
|
804
|
+
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
km.dispatch( 'shade' );
|
|
808
|
+
km.dispatch( 'compact' );
|
|
809
|
+
km.dispatch( dynamic ? 'lightCopyback' : 'lightSnapshot' );
|
|
810
|
+
|
|
811
|
+
if ( exitEnergy >= 0 && bounce < loopBound && energy?.[ bounce ] !== undefined && energy[ bounce ] <= exitEnergy ) break;
|
|
812
|
+
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if ( this.uniforms.lightTrace.value > 0 ) {
|
|
816
|
+
|
|
817
|
+
km.setDispatchForCount( 'lightSplat', paths * this.uniforms.slotsPerPath.value );
|
|
818
|
+
km.dispatch( 'lightSplat' );
|
|
819
|
+
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if ( this.merging ) {
|
|
823
|
+
|
|
824
|
+
km.dispatch( 'mergeClear' );
|
|
825
|
+
km.setDispatchForCount( 'mergeInsert', paths * this.uniforms.slotsPerPath.value );
|
|
826
|
+
km.dispatch( 'mergeInsert' );
|
|
827
|
+
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
}
|