@driftengine/core 4.3.0 → 4.4.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/CHANGELOG.json +282 -0
- package/README.md +1 -1
- package/dist/cinematic/cameraPath.d.ts +53 -0
- package/dist/cinematic/cameraPath.js +118 -0
- package/dist/environment/daylight.d.ts +77 -0
- package/dist/environment/daylight.js +77 -0
- package/dist/geometry/meshConcat.d.ts +9 -0
- package/dist/geometry/meshConcat.js +101 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/dist/render/ambientOcclusionPass.js +5 -1
- package/dist/render/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +147 -1
- package/dist/render/backend/webgl2/renderer.js +402 -23
- package/dist/render/backend/webgpu/buffers.js +46 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
- package/dist/render/backend/webgpu/depthPass.js +75 -14
- package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
- package/dist/render/backend/webgpu/exposurePass.js +190 -0
- package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
- package/dist/render/backend/webgpu/fieldCompose.js +13 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
- package/dist/render/backend/webgpu/flatPass.js +12 -5
- package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
- package/dist/render/backend/webgpu/particlePass.js +1 -0
- package/dist/render/backend/webgpu/postPass.d.ts +13 -3
- package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
- package/dist/render/backend/webgpu/probeBake.js +97 -7
- package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
- package/dist/render/backend/webgpu/probeTargets.js +180 -0
- package/dist/render/backend/webgpu/renderer.d.ts +174 -11
- package/dist/render/backend/webgpu/renderer.js +682 -252
- package/dist/render/backend/webgpu/scatterPass.js +7 -7
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
- package/dist/render/backend/webgpu/skyPass.js +5 -1
- package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
- package/dist/render/backend/webgpu/uniformRing.js +53 -3
- package/dist/render/bloomChain.d.ts +6 -0
- package/dist/render/bloomChain.js +8 -0
- package/dist/render/clusteredLights.d.ts +19 -11
- package/dist/render/clusteredLights.js +96 -45
- package/dist/render/cutoutCaster.d.ts +23 -0
- package/dist/render/cutoutCaster.js +20 -0
- package/dist/render/driftLight/bake.d.ts +34 -0
- package/dist/render/driftLight/bake.js +95 -0
- package/dist/render/driftLight/falloff.d.ts +16 -0
- package/dist/render/driftLight/falloff.js +23 -0
- package/dist/render/driftLight/layout.d.ts +59 -0
- package/dist/render/driftLight/layout.js +138 -0
- package/dist/render/driftLight/lightField.d.ts +108 -0
- package/dist/render/driftLight/lightField.js +179 -0
- package/dist/render/driftLight/uniforms.d.ts +28 -0
- package/dist/render/driftLight/uniforms.js +41 -0
- package/dist/render/driftLight/visibility.d.ts +18 -0
- package/dist/render/driftLight/visibility.js +47 -0
- package/dist/render/emptyTexture.d.ts +6 -0
- package/dist/render/emptyTexture.js +19 -0
- package/dist/render/exposurePass.d.ts +48 -0
- package/dist/render/exposurePass.js +172 -0
- package/dist/render/flameLight.d.ts +71 -0
- package/dist/render/flameLight.js +63 -0
- package/dist/render/gi/bounceLights.d.ts +34 -0
- package/dist/render/gi/bounceLights.js +66 -0
- package/dist/render/gi/fieldReach.d.ts +12 -0
- package/dist/render/gi/fieldReach.js +62 -0
- package/dist/render/gi/globalField.d.ts +8 -0
- package/dist/render/gi/globalField.js +31 -0
- package/dist/render/gi/probeTrace.d.ts +3 -1
- package/dist/render/gi/probeTrace.js +3 -1
- package/dist/render/gpuCapability.js +19 -4
- package/dist/render/lightBudget.d.ts +11 -0
- package/dist/render/lightBudget.js +11 -0
- package/dist/render/pointLightSelection.d.ts +16 -0
- package/dist/render/pointLightSelection.js +178 -83
- package/dist/render/pointShadowBudget.d.ts +12 -2
- package/dist/render/pointShadowBudget.js +16 -10
- package/dist/render/probeGrid.d.ts +11 -1
- package/dist/render/probeGrid.js +5 -2
- package/dist/render/probeSweeps.d.ts +45 -0
- package/dist/render/probeSweeps.js +77 -0
- package/dist/render/reflectionProbe.d.ts +43 -1
- package/dist/render/reflectionProbe.js +17 -3
- package/dist/render/renderQuality.d.ts +3 -1
- package/dist/render/rendererHarness.d.ts +9 -0
- package/dist/render/rendererHarness.js +18 -3
- package/dist/render/sceneTarget.d.ts +20 -0
- package/dist/render/sceneTarget.js +62 -1
- package/dist/render/shaders/ambientOcclusion.d.ts +63 -6
- package/dist/render/shaders/ambientOcclusion.js +188 -71
- package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
- package/dist/render/shaders/cutoutCoverage.js +40 -0
- package/dist/render/shaders/depth.d.ts +25 -0
- package/dist/render/shaders/depth.js +69 -3
- package/dist/render/shaders/exposure.d.ts +52 -0
- package/dist/render/shaders/exposure.js +124 -0
- package/dist/render/shaders/filmLook.d.ts +47 -0
- package/dist/render/shaders/filmLook.js +98 -0
- package/dist/render/shaders/flat/directionalShadow.js +35 -4
- package/dist/render/shaders/flat/driftLight.d.ts +29 -0
- package/dist/render/shaders/flat/driftLight.js +75 -0
- package/dist/render/shaders/flat/index.js +5 -0
- package/dist/render/shaders/flat/lobes.js +13 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +68 -9
- package/dist/render/shaders/flat/preamble.js +23 -5
- package/dist/render/shaders/flat/probeGrid.js +46 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
- package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
- package/dist/render/shaders/generated/depth.wgsl.js +100 -0
- package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
- package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
- package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
- package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
- package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
- package/dist/render/shaders/generated/rush.wgsl.js +40 -10
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
- package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
- package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
- package/dist/render/shaders/globalMedium.js +4 -0
- package/dist/render/shaders/localExposure.d.ts +35 -0
- package/dist/render/shaders/localExposure.js +138 -0
- package/dist/render/shaders/prefilter.d.ts +1 -1
- package/dist/render/shaders/prefilter.js +6 -0
- package/dist/render/shaders/rush.js +33 -5
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +15 -3
- package/dist/render/shaders/temporalResolve.js +39 -1
- package/dist/render/shadowCasters.d.ts +5 -3
- package/dist/render/shadowMap.js +13 -0
- package/dist/render/surfaceTexture.d.ts +5 -0
- package/package.json +6 -6
- package/scripts/gpuCompute.mjs +24 -3
- package/scripts/png.mjs +23 -12
- package/src/cinematic/cameraPath.ts +178 -0
- package/src/environment/daylight.ts +152 -0
- package/src/geometry/meshConcat.ts +100 -0
- package/src/index.ts +32 -0
- package/src/render/ambientOcclusionPass.ts +5 -1
- package/src/render/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +439 -20
- package/src/render/backend/webgpu/buffers.ts +51 -8
- package/src/render/backend/webgpu/depthPass.ts +78 -10
- package/src/render/backend/webgpu/exposurePass.ts +234 -0
- package/src/render/backend/webgpu/fieldCompose.ts +15 -0
- package/src/render/backend/webgpu/flatPass.ts +10 -2
- package/src/render/backend/webgpu/particlePass.ts +9 -0
- package/src/render/backend/webgpu/probeBake.ts +143 -9
- package/src/render/backend/webgpu/probeTargets.ts +211 -0
- package/src/render/backend/webgpu/renderer.ts +808 -273
- package/src/render/backend/webgpu/scatterPass.ts +7 -7
- package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
- package/src/render/backend/webgpu/skyPass.ts +5 -1
- package/src/render/backend/webgpu/uniformRing.ts +49 -3
- package/src/render/bloomChain.ts +9 -0
- package/src/render/clusteredLights.ts +108 -63
- package/src/render/cutoutCaster.ts +44 -0
- package/src/render/driftLight/bake.ts +125 -0
- package/src/render/driftLight/falloff.ts +26 -0
- package/src/render/driftLight/layout.ts +177 -0
- package/src/render/driftLight/lightField.ts +227 -0
- package/src/render/driftLight/uniforms.ts +61 -0
- package/src/render/driftLight/visibility.ts +58 -0
- package/src/render/emptyTexture.ts +41 -0
- package/src/render/exposurePass.ts +186 -0
- package/src/render/flameLight.ts +135 -0
- package/src/render/gi/bounceLights.ts +68 -0
- package/src/render/gi/fieldReach.ts +74 -0
- package/src/render/gi/globalField.ts +39 -0
- package/src/render/gi/probeTrace.ts +3 -1
- package/src/render/gpuCapability.ts +18 -5
- package/src/render/lightBudget.ts +12 -0
- package/src/render/pointLightSelection.ts +191 -79
- package/src/render/pointShadowBudget.ts +16 -8
- package/src/render/probeGrid.ts +13 -2
- package/src/render/probeSweeps.ts +80 -0
- package/src/render/reflectionProbe.ts +54 -2
- package/src/render/renderQuality.ts +3 -1
- package/src/render/rendererHarness.ts +25 -3
- package/src/render/sceneTarget.ts +75 -1
- package/src/render/shaders/ambientOcclusion.ts +205 -71
- package/src/render/shaders/cutoutCoverage.ts +43 -0
- package/src/render/shaders/depth.ts +80 -3
- package/src/render/shaders/exposure.ts +128 -0
- package/src/render/shaders/filmLook.ts +111 -0
- package/src/render/shaders/flat/directionalShadow.ts +35 -4
- package/src/render/shaders/flat/driftLight.ts +75 -0
- package/src/render/shaders/flat/index.ts +5 -0
- package/src/render/shaders/flat/lobes.ts +13 -0
- package/src/render/shaders/flat/main.ts +68 -9
- package/src/render/shaders/flat/preamble.ts +23 -5
- package/src/render/shaders/flat/probeGrid.ts +46 -3
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
- package/src/render/shaders/generated/depth.wgsl.ts +103 -0
- package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
- package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
- package/src/render/shaders/generated/rush.wgsl.ts +40 -10
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
- package/src/render/shaders/globalMedium.ts +4 -0
- package/src/render/shaders/localExposure.ts +154 -0
- package/src/render/shaders/prefilter.ts +6 -0
- package/src/render/shaders/rush.ts +33 -5
- package/src/render/shaders/tangentFrame.ts +15 -3
- package/src/render/shaders/temporalResolve.ts +39 -1
- package/src/render/shadowCasters.ts +5 -3
- package/src/render/shadowMap.ts +14 -0
- package/src/render/surfaceTexture.ts +5 -0
|
@@ -165,18 +165,18 @@ export function createScatterBindGroupLayout(device: GPUDevice): GPUBindGroupLay
|
|
|
165
165
|
label: 'scatter.layout',
|
|
166
166
|
entries: [
|
|
167
167
|
/*
|
|
168
|
-
* **
|
|
168
|
+
* **Both blocks are per draw.**
|
|
169
169
|
*
|
|
170
170
|
* The vertex block carries the gust and the trample field, and both arrive as arguments
|
|
171
171
|
* to `drawScatter` — so a scene bending its grass by one press field and its canopy by
|
|
172
172
|
* another gets whichever was written last for both, from a single buffer. That is not
|
|
173
173
|
* hypothetical elsewhere: the plume pass had exactly this and drew a flame bent by the
|
|
174
|
-
* smoke's wind response.
|
|
175
|
-
* batches the same wind and no trample, which is luck rather than a guarantee.
|
|
174
|
+
* smoke's wind response.
|
|
176
175
|
*
|
|
177
|
-
* The fragment block holds the camera, the sun, the point lights and the medium
|
|
178
|
-
* are settled per pass
|
|
179
|
-
*
|
|
176
|
+
* The fragment block holds the camera, the sun, the point lights and the medium. It was one
|
|
177
|
+
* buffer on the argument that those are settled per pass, and a pass is not an encoder: a
|
|
178
|
+
* probe records six passes in one, a mirror shares the frame's, and queue writes land before
|
|
179
|
+
* either runs. So every pass drew its blades from the last camera, and it is a ring too.
|
|
180
180
|
*/
|
|
181
181
|
{
|
|
182
182
|
binding: VERT_BINDING,
|
|
@@ -186,7 +186,7 @@ export function createScatterBindGroupLayout(device: GPUDevice): GPUBindGroupLay
|
|
|
186
186
|
{
|
|
187
187
|
binding: FRAG_BINDING,
|
|
188
188
|
visibility: VISIBILITY_FRAGMENT,
|
|
189
|
-
buffer: { type: 'uniform', minBindingSize: SCATTER_FRAG_SIZE },
|
|
189
|
+
buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: SCATTER_FRAG_SIZE },
|
|
190
190
|
},
|
|
191
191
|
],
|
|
192
192
|
});
|
|
@@ -63,6 +63,16 @@ export class SkinPaletteRing {
|
|
|
63
63
|
this.used = 0;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** Where the ring stands, to hand back to `rewind`. See `UniformRing.rewind` for when it is safe. */
|
|
67
|
+
mark(): number {
|
|
68
|
+
return this.used;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Give back every slot taken since `mark`, once the commands reading them are submitted. */
|
|
72
|
+
rewind(mark: number): void {
|
|
73
|
+
if (mark < this.used) this.used = mark;
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
/**
|
|
67
77
|
* Upload a palette into a slot of its own and return that slot, or null when the ring is full.
|
|
68
78
|
*
|
|
@@ -26,7 +26,11 @@ export function createSkyBindGroupLayout(device: GPUDevice): GPUBindGroupLayout
|
|
|
26
26
|
{
|
|
27
27
|
binding: SKY_BINDING,
|
|
28
28
|
visibility: VISIBILITY_FRAGMENT,
|
|
29
|
-
|
|
29
|
+
/*
|
|
30
|
+
* A dynamic offset, because a frame draws more than one sky — a probe's six faces, a mirror
|
|
31
|
+
* and the view — and one buffer rewritten between them hands every draw the last write.
|
|
32
|
+
*/
|
|
33
|
+
buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: SKY_UNIFORM_SIZE },
|
|
30
34
|
},
|
|
31
35
|
],
|
|
32
36
|
});
|
|
@@ -58,6 +58,11 @@ export class UniformRing {
|
|
|
58
58
|
private ints: Int32Array;
|
|
59
59
|
private capacity: number;
|
|
60
60
|
private used = 0;
|
|
61
|
+
/**
|
|
62
|
+
* The byte below which the device already holds what the staging does: the end of the last
|
|
63
|
+
* flush, lowered by any write under it since. See `flush`.
|
|
64
|
+
*/
|
|
65
|
+
private clean = 0;
|
|
61
66
|
|
|
62
67
|
/**
|
|
63
68
|
* `label` names the buffer on the device, and it is not decoration.
|
|
@@ -124,6 +129,7 @@ export class UniformRing {
|
|
|
124
129
|
|
|
125
130
|
this.buffer.destroy();
|
|
126
131
|
this.capacity = slots;
|
|
132
|
+
this.clean = 0;
|
|
127
133
|
this.staging = new ArrayBuffer(bytes);
|
|
128
134
|
this.floats = new Float32Array(this.staging);
|
|
129
135
|
this.ints = new Int32Array(this.staging);
|
|
@@ -143,6 +149,28 @@ export class UniformRing {
|
|
|
143
149
|
/** Start a frame. Slots are reused; nothing is freed. */
|
|
144
150
|
reset(): void {
|
|
145
151
|
this.used = 0;
|
|
152
|
+
this.clean = 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Where the ring stands, to hand back to `rewind`. */
|
|
156
|
+
mark(): number {
|
|
157
|
+
return this.used;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Give back every slot taken since `mark`, for a caller that has already submitted the commands
|
|
162
|
+
* reading them.
|
|
163
|
+
*
|
|
164
|
+
* **Safe because the queue is ordered, and only then.** A `writeBuffer` issued after a submit
|
|
165
|
+
* cannot reach the commands in it, so slots a submitted encoder read may be rewritten by the next
|
|
166
|
+
* one. A slot taken by an encoder not yet submitted may not be: every draw pointing at it would
|
|
167
|
+
* read whatever overwrote it, which is the defect this class exists to prevent. What uses it is
|
|
168
|
+
* work that submits encoders of its own mid-frame — a probe bake — so the frame's rings are not
|
|
169
|
+
* spent by something the frame never draws.
|
|
170
|
+
*/
|
|
171
|
+
rewind(mark: number): void {
|
|
172
|
+
if (mark < this.used) this.used = mark;
|
|
173
|
+
if (this.used * this.slotSize < this.clean) this.clean = this.used * this.slotSize;
|
|
146
174
|
}
|
|
147
175
|
|
|
148
176
|
/**
|
|
@@ -180,6 +208,7 @@ export class UniformRing {
|
|
|
180
208
|
|
|
181
209
|
/** Write floats into a slot, at a byte offset within it. */
|
|
182
210
|
writeFloats(slot: number, byteOffset: number, values: ArrayLike<number>): void {
|
|
211
|
+
if (slot < this.clean) this.clean = slot;
|
|
183
212
|
this.floats.set(values, (slot + byteOffset) / 4);
|
|
184
213
|
}
|
|
185
214
|
|
|
@@ -192,11 +221,13 @@ export class UniformRing {
|
|
|
192
221
|
* buffer being reused.
|
|
193
222
|
*/
|
|
194
223
|
writeFloat(slot: number, byteOffset: number, value: number): void {
|
|
224
|
+
if (slot < this.clean) this.clean = slot;
|
|
195
225
|
this.floats[(slot + byteOffset) / 4] = value;
|
|
196
226
|
}
|
|
197
227
|
|
|
198
228
|
/** Write one integer into a slot. Separate because a `Float32Array` cannot hold an `i32`. */
|
|
199
229
|
writeInt(slot: number, byteOffset: number, value: number): void {
|
|
230
|
+
if (slot < this.clean) this.clean = slot;
|
|
200
231
|
this.ints[(slot + byteOffset) / 4] = value;
|
|
201
232
|
}
|
|
202
233
|
|
|
@@ -213,13 +244,28 @@ export class UniformRing {
|
|
|
213
244
|
* `Int32Array.set` from an `Int32Array` is an exact copy of every bit, whatever the bits mean.
|
|
214
245
|
*/
|
|
215
246
|
writeBlock(slot: number, block: Int32Array): void {
|
|
247
|
+
if (slot < this.clean) this.clean = slot;
|
|
216
248
|
this.ints.set(block, slot / 4);
|
|
217
249
|
}
|
|
218
250
|
|
|
219
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* Upload every slot taken this frame that the device does not already hold, in one write.
|
|
253
|
+
*
|
|
254
|
+
* **From the watermark, not from zero, because a frame flushes more than once.** Every submit in
|
|
255
|
+
* the middle of a frame flushes first, and a probe bake submits per face: sending the whole used
|
|
256
|
+
* range each time sent a courtyard's first slots two hundred times over, 2.3 GB in a two-second
|
|
257
|
+
* grid bake, measured. Queue writes are ordered, so bytes already sent and not written since are
|
|
258
|
+
* on the device for every command after them, and resending them changed nothing.
|
|
259
|
+
*
|
|
260
|
+
* What it gives up is one compare per write. What would make it wrong is a write that reached the
|
|
261
|
+
* staging without passing a method that lowers `clean` — which is why the staging stays private.
|
|
262
|
+
*/
|
|
220
263
|
flush(): void {
|
|
221
|
-
|
|
222
|
-
|
|
264
|
+
const end = this.used * this.slotSize;
|
|
265
|
+
if (end <= this.clean) return;
|
|
266
|
+
const from = this.clean;
|
|
267
|
+
this.device.queue.writeBuffer(this.buffer, from, this.staging, from, end - from);
|
|
268
|
+
this.clean = end;
|
|
223
269
|
}
|
|
224
270
|
|
|
225
271
|
dispose(): void {
|
package/src/render/bloomChain.ts
CHANGED
|
@@ -49,6 +49,15 @@ export const BLOOM_FILTER_RADIUS_UV = 0.005;
|
|
|
49
49
|
* an exposure with no curve to be exposed into — where everything is switched on, nothing is
|
|
50
50
|
* wrong, and nothing happens.
|
|
51
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* A threshold a frame asked for through `setBloom`, made safe: at least a thousandth of a scene
|
|
54
|
+
* unit, because zero blooms the whole frame including what is meant to be dark, and a
|
|
55
|
+
* non-number would reach the shader as one. Decided here so both backends answer the same.
|
|
56
|
+
*/
|
|
57
|
+
export function bloomThresholdOf(threshold: number): number {
|
|
58
|
+
return Number.isFinite(threshold) ? Math.max(threshold, 1e-3) : 1e-3;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
export function bloomProfileWarning(quality: {
|
|
53
62
|
readonly bloom: number;
|
|
54
63
|
readonly screenEffects: boolean;
|
|
@@ -24,27 +24,35 @@ export const CLUSTER_Z = 24;
|
|
|
24
24
|
export const CLUSTER_COUNT = CLUSTER_X * CLUSTER_Y * CLUSTER_Z;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Texels a cluster occupies: one for its count,
|
|
27
|
+
* Texels a cluster occupies: one for its count, nineteen holding four light indices each.
|
|
28
28
|
*
|
|
29
29
|
* **Fixed rather than packed, and that is what removes the atomics.** One invocation owns one
|
|
30
30
|
* cluster and writes only its own run, so nothing is appended to a shared list, there is no
|
|
31
31
|
* counter to increment atomically and no compaction pass. That is also what lets the GPU binner
|
|
32
32
|
* write through a *write-only* storage texture, which is all core WGSL gives.
|
|
33
33
|
*/
|
|
34
|
-
export const CLUSTER_TEXELS =
|
|
34
|
+
export const CLUSTER_TEXELS = 20;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* How many lights one cluster can hold
|
|
37
|
+
* How many lights one cluster can hold: nineteen index texels at four indices each, 76.
|
|
38
38
|
*
|
|
39
|
-
* **Sixteen
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
* **Sixteen until a candlelit interior drew its froxels as rectangles.** The cap was pinned to
|
|
40
|
+
* `MAX_POINT_LIGHTS` so the one light loop kept one bound. Then a courtyard of candles, lanterns
|
|
41
|
+
* and braziers asked 461 of its 3,456 froxels for more than sixteen, and up to fifty. A full froxel
|
|
42
|
+
* keeps its nearest and drops the rest, and its neighbour keeps a different set, so the step
|
|
43
|
+
* between them drew as hard-edged, screen-aligned rectangles on every vault, on both backends.
|
|
44
|
+
*
|
|
45
|
+
* Seventy-six because it is the next cap the table's shape allows above what was measured: a run
|
|
46
|
+
* must divide the 320-texel row, and the two regions must end on one. What it costs is the table,
|
|
47
|
+
* 292 KB to 1.1 MB, and the loop's bound. The bound is the clustered arm's alone in effect: the
|
|
48
|
+
* fixed arm still leaves at its own count and never passes `MAX_LIGHTS`, so a consumer that does
|
|
49
|
+
* not cluster iterates what it did. What would make 76 wrong is content that asks more of one
|
|
50
|
+
* froxel, and the answer then is the next cap rather than a softer overflow: any overflow rule
|
|
51
|
+
* draws the cluster's edge.
|
|
44
52
|
*
|
|
45
53
|
* **It is not the old limit wearing a new hat.** Sixteen was a budget for the *whole scene*, chosen
|
|
46
|
-
* by distance with a contention band because somebody always had to be last.
|
|
47
|
-
*
|
|
54
|
+
* by distance with a contention band because somebody always had to be last. This is a different
|
|
55
|
+
* set in every froxel, out of `MAX_CLUSTERED_LIGHTS` in the scene.
|
|
48
56
|
*/
|
|
49
57
|
export const MAX_LIGHTS_PER_CLUSTER = (CLUSTER_TEXELS - 1) * 4;
|
|
50
58
|
|
|
@@ -162,7 +170,7 @@ export const LIGHT_REGION_TEXELS = MAX_CLUSTERED_LIGHTS * LIGHT_TEXELS;
|
|
|
162
170
|
*/
|
|
163
171
|
export const TABLE_WIDTH = 320;
|
|
164
172
|
|
|
165
|
-
/** Rows:
|
|
173
|
+
/** Rows: five of light records and 216 of cluster runs. 1.1 MB of `RGBA32UI`, allocated once. */
|
|
166
174
|
export const TABLE_HEIGHT = (LIGHT_REGION_TEXELS + CLUSTER_COUNT * CLUSTER_TEXELS) / TABLE_WIDTH;
|
|
167
175
|
|
|
168
176
|
/** The table, as the `Uint32Array` both a texture upload and a readback comparison want. */
|
|
@@ -247,6 +255,33 @@ export function clusterViewBounds(
|
|
|
247
255
|
out[5] = zFar;
|
|
248
256
|
}
|
|
249
257
|
|
|
258
|
+
/**
|
|
259
|
+
* `clusterViewBounds` for a slice whose depths and half extents the caller already has. The same
|
|
260
|
+
* arithmetic in the same order, so the same box to the bit; the binner's inner loop uses this.
|
|
261
|
+
*/
|
|
262
|
+
function sliceBounds(
|
|
263
|
+
i: number,
|
|
264
|
+
j: number,
|
|
265
|
+
zNear: number,
|
|
266
|
+
zFar: number,
|
|
267
|
+
halfWNear: number,
|
|
268
|
+
halfWFar: number,
|
|
269
|
+
halfHNear: number,
|
|
270
|
+
halfHFar: number,
|
|
271
|
+
out: Float32Array,
|
|
272
|
+
): void {
|
|
273
|
+
const xLo = -1 + (2 * i) / CLUSTER_X;
|
|
274
|
+
const xHi = -1 + (2 * (i + 1)) / CLUSTER_X;
|
|
275
|
+
const yLo = -1 + (2 * j) / CLUSTER_Y;
|
|
276
|
+
const yHi = -1 + (2 * (j + 1)) / CLUSTER_Y;
|
|
277
|
+
out[0] = Math.min(xLo * halfWNear, xLo * halfWFar, xHi * halfWNear, xHi * halfWFar);
|
|
278
|
+
out[3] = Math.max(xLo * halfWNear, xLo * halfWFar, xHi * halfWNear, xHi * halfWFar);
|
|
279
|
+
out[1] = Math.min(yLo * halfHNear, yLo * halfHFar, yHi * halfHNear, yHi * halfHFar);
|
|
280
|
+
out[4] = Math.max(yLo * halfHNear, yLo * halfHFar, yHi * halfHNear, yHi * halfHFar);
|
|
281
|
+
out[2] = zNear;
|
|
282
|
+
out[5] = zFar;
|
|
283
|
+
}
|
|
284
|
+
|
|
250
285
|
/** The light arrays the binner reads, in the shape `Environment` already carries them. */
|
|
251
286
|
export interface ClusterLightSet {
|
|
252
287
|
readonly count: number;
|
|
@@ -282,6 +317,16 @@ export interface ClusterLightSet {
|
|
|
282
317
|
* rule forbids allocating there.
|
|
283
318
|
*/
|
|
284
319
|
const bounds = new Float32Array(6);
|
|
320
|
+
/**
|
|
321
|
+
* Where each slice begins, for the frame being binned: `sliceNearDepth` for every slice and the
|
|
322
|
+
* far plane, computed once. The same expression as a call per cluster, so the same numbers.
|
|
323
|
+
*
|
|
324
|
+
* **Once a binning rather than once a cluster**, which was two `pow` for every light and every
|
|
325
|
+
* cluster it might touch. A courtyard of candles within a few metres of the camera is 640,000 of
|
|
326
|
+
* those pairs, and the binning took 47 ms of a frame. On WebGL2 it runs every `bindMeshPass`, so a
|
|
327
|
+
* probe grid baked at night binned for a minute.
|
|
328
|
+
*/
|
|
329
|
+
const sliceDepths = new Float64Array(CLUSTER_Z + 1);
|
|
285
330
|
/** View-space light centres, kept for the overflow rule, which needs them after the fact. */
|
|
286
331
|
const viewX = new Float32Array(MAX_CLUSTERED_LIGHTS);
|
|
287
332
|
const viewY = new Float32Array(MAX_CLUSTERED_LIGHTS);
|
|
@@ -312,19 +357,6 @@ function distanceSqToBounds(x: number, y: number, z: number, b: Float32Array): n
|
|
|
312
357
|
return dx * dx + dy * dy + dz * dz;
|
|
313
358
|
}
|
|
314
359
|
|
|
315
|
-
/** Insertion sort over a cluster's index slots. At most 28 integers, so this is not a hot loop. */
|
|
316
|
-
function sortIndices(table: Uint32Array, base: number, count: number): void {
|
|
317
|
-
for (let n = 1; n < count; n++) {
|
|
318
|
-
const value = table[base + 4 + n] ?? 0;
|
|
319
|
-
let m = n - 1;
|
|
320
|
-
while (m >= 0 && (table[base + 4 + m] ?? 0) > value) {
|
|
321
|
-
table[base + 4 + m + 1] = table[base + 4 + m] ?? 0;
|
|
322
|
-
m--;
|
|
323
|
-
}
|
|
324
|
-
table[base + 4 + m + 1] = value;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
360
|
/**
|
|
329
361
|
* Fill the table with this frame's light assignment, and answer how many lights it carried.
|
|
330
362
|
*
|
|
@@ -393,6 +425,7 @@ export function buildLightClusters(
|
|
|
393
425
|
* replaces; agreeing about the invisible bytes is what keeps the comparison exact.
|
|
394
426
|
*/
|
|
395
427
|
table.fill(0, LIGHT_REGION_TEXELS * 4);
|
|
428
|
+
for (let k = 0; k <= CLUSTER_Z; k++) sliceDepths[k] = sliceNearDepth(k, near, far);
|
|
396
429
|
|
|
397
430
|
for (let light = 0; light < count; light++) {
|
|
398
431
|
const x = lights.positions[light * 3] ?? 0;
|
|
@@ -492,8 +525,10 @@ export function buildLightClusters(
|
|
|
492
525
|
const radiusSq = radius * radius;
|
|
493
526
|
for (let k = kLo; k <= kHi; k++) {
|
|
494
527
|
/* The same two depths `clusterViewBounds` builds this slice's box from. */
|
|
495
|
-
const
|
|
496
|
-
const
|
|
528
|
+
const zNear = sliceDepths[k] ?? 0;
|
|
529
|
+
const zFar = sliceDepths[k + 1] ?? 0;
|
|
530
|
+
const halfHNear = zNear * tanHalfFovY;
|
|
531
|
+
const halfHFar = zFar * tanHalfFovY;
|
|
497
532
|
const halfWNear = halfHNear * aspect;
|
|
498
533
|
const halfWFar = halfHFar * aspect;
|
|
499
534
|
const iLo = tileOf(
|
|
@@ -514,17 +549,9 @@ export function buildLightClusters(
|
|
|
514
549
|
);
|
|
515
550
|
for (let j = jLo; j <= jHi; j++) {
|
|
516
551
|
for (let i = iLo; i <= iHi; i++) {
|
|
517
|
-
|
|
552
|
+
sliceBounds(i, j, zNear, zFar, halfWNear, halfWFar, halfHNear, halfHFar, bounds);
|
|
518
553
|
if (distanceSqToBounds(vx, vy, depth, bounds) > radiusSq) continue;
|
|
519
|
-
insert(
|
|
520
|
-
table,
|
|
521
|
-
i + j * CLUSTER_X + k * CLUSTER_X * CLUSTER_Y,
|
|
522
|
-
light,
|
|
523
|
-
near,
|
|
524
|
-
far,
|
|
525
|
-
tanHalfFovY,
|
|
526
|
-
aspect,
|
|
527
|
-
);
|
|
554
|
+
insert(table, i + j * CLUSTER_X + k * CLUSTER_X * CLUSTER_Y, light, bounds);
|
|
528
555
|
}
|
|
529
556
|
}
|
|
530
557
|
}
|
|
@@ -539,47 +566,45 @@ function tileOf(ndc: number, tiles: number): number {
|
|
|
539
566
|
}
|
|
540
567
|
|
|
541
568
|
/**
|
|
542
|
-
*
|
|
569
|
+
* Each member's squared distance from its cluster's centre, slot for slot with the table, for the
|
|
570
|
+
* frame being binned. Allocated on the first binning, so a consumer that never clusters pays nothing.
|
|
571
|
+
*
|
|
572
|
+
* **Measured rather than assumed rare.** The overflow rule used to recompute the cluster's box and
|
|
573
|
+
* all sixteen members' distances on every insert into a full cluster, on the argument that full
|
|
574
|
+
* clusters are rare. Three hundred candles within a few metres of the camera fill 2,152 of the
|
|
575
|
+
* 3,456, and the binning spent 47 ms a frame there. Each distance is the same number whenever it is
|
|
576
|
+
* computed, so it is computed once, at the insert that placed the member.
|
|
577
|
+
*/
|
|
578
|
+
let memberDistance: Float64Array | null = null;
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Put a light into a cluster, applying the overflow rule. `box` is the cluster's bounds, which the
|
|
582
|
+
* caller has just tested the light against.
|
|
543
583
|
*
|
|
544
584
|
* Lights arrive in increasing index, so the common path appends and the list is already sorted.
|
|
545
585
|
* The sort only runs on the rare path where a replacement broke that order.
|
|
546
586
|
*/
|
|
547
|
-
function insert(
|
|
548
|
-
table: Uint32Array,
|
|
549
|
-
cluster: number,
|
|
550
|
-
light: number,
|
|
551
|
-
near: number,
|
|
552
|
-
far: number,
|
|
553
|
-
tanHalfFovY: number,
|
|
554
|
-
aspect: number,
|
|
555
|
-
): void {
|
|
587
|
+
function insert(table: Uint32Array, cluster: number, light: number, box: Float32Array): void {
|
|
556
588
|
const base = clusterBase(cluster);
|
|
557
589
|
const count = table[base] ?? 0;
|
|
590
|
+
const distances = (memberDistance ??= new Float64Array(CLUSTER_COUNT * MAX_LIGHTS_PER_CLUSTER));
|
|
591
|
+
const at = cluster * MAX_LIGHTS_PER_CLUSTER;
|
|
592
|
+
const cx = ((box[0] ?? 0) + (box[3] ?? 0)) * 0.5;
|
|
593
|
+
const cy = ((box[1] ?? 0) + (box[4] ?? 0)) * 0.5;
|
|
594
|
+
const cz = ((box[2] ?? 0) + (box[5] ?? 0)) * 0.5;
|
|
595
|
+
const mine = centreDistanceSq(light, cx, cy, cz);
|
|
558
596
|
if (count < MAX_LIGHTS_PER_CLUSTER) {
|
|
559
597
|
table[base + 4 + count] = light;
|
|
598
|
+
distances[at + count] = mine;
|
|
560
599
|
table[base] = count + 1;
|
|
561
600
|
return;
|
|
562
601
|
}
|
|
563
602
|
|
|
564
|
-
/*
|
|
565
|
-
* Full. Drop the farthest member if this one is nearer, measured from the cluster's own centre.
|
|
566
|
-
* Rare enough that recomputing the members' distances beats carrying 96,768 floats of scratch
|
|
567
|
-
* for a case most frames never reach.
|
|
568
|
-
*/
|
|
569
|
-
const k = Math.floor(cluster / (CLUSTER_X * CLUSTER_Y));
|
|
570
|
-
const j = Math.floor((cluster - k * CLUSTER_X * CLUSTER_Y) / CLUSTER_X);
|
|
571
|
-
const i = cluster - k * CLUSTER_X * CLUSTER_Y - j * CLUSTER_X;
|
|
572
|
-
clusterViewBounds(i, j, k, near, far, tanHalfFovY, aspect, bounds);
|
|
573
|
-
const cx = ((bounds[0] ?? 0) + (bounds[3] ?? 0)) * 0.5;
|
|
574
|
-
const cy = ((bounds[1] ?? 0) + (bounds[4] ?? 0)) * 0.5;
|
|
575
|
-
const cz = ((bounds[2] ?? 0) + (bounds[5] ?? 0)) * 0.5;
|
|
576
|
-
|
|
577
|
-
const mine = centreDistanceSq(light, cx, cy, cz);
|
|
603
|
+
/* Full. Drop the farthest member if this one is nearer, measured from the cluster's own centre. */
|
|
578
604
|
let worstSlot = -1;
|
|
579
605
|
let worst = mine;
|
|
580
606
|
for (let n = 0; n < MAX_LIGHTS_PER_CLUSTER; n++) {
|
|
581
|
-
const
|
|
582
|
-
const d = centreDistanceSq(held, cx, cy, cz);
|
|
607
|
+
const d = distances[at + n] ?? 0;
|
|
583
608
|
if (d > worst) {
|
|
584
609
|
worst = d;
|
|
585
610
|
worstSlot = n;
|
|
@@ -587,7 +612,27 @@ function insert(
|
|
|
587
612
|
}
|
|
588
613
|
if (worstSlot < 0) return;
|
|
589
614
|
table[base + 4 + worstSlot] = light;
|
|
590
|
-
|
|
615
|
+
distances[at + worstSlot] = mine;
|
|
616
|
+
sortMembers(table, base, distances, at);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Sort a full cluster's index slots ascending, carrying each member's distance with it. An insertion
|
|
621
|
+
* sort: sixteen integers, so this is not a hot loop.
|
|
622
|
+
*/
|
|
623
|
+
function sortMembers(table: Uint32Array, base: number, distances: Float64Array, at: number): void {
|
|
624
|
+
for (let n = 1; n < MAX_LIGHTS_PER_CLUSTER; n++) {
|
|
625
|
+
const value = table[base + 4 + n] ?? 0;
|
|
626
|
+
const distance = distances[at + n] ?? 0;
|
|
627
|
+
let m = n - 1;
|
|
628
|
+
while (m >= 0 && (table[base + 4 + m] ?? 0) > value) {
|
|
629
|
+
table[base + 4 + m + 1] = table[base + 4 + m] ?? 0;
|
|
630
|
+
distances[at + m + 1] = distances[at + m] ?? 0;
|
|
631
|
+
m--;
|
|
632
|
+
}
|
|
633
|
+
table[base + 4 + m + 1] = value;
|
|
634
|
+
distances[at + m + 1] = distance;
|
|
635
|
+
}
|
|
591
636
|
}
|
|
592
637
|
|
|
593
638
|
function centreDistanceSq(light: number, cx: number, cy: number, cz: number): number {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which shadow casters cut their shadow out of their texture's alpha, decided once for both backends.
|
|
3
|
+
*
|
|
4
|
+
* **A caster with an albedo and a cutoff above zero is a cutout**: a leaf card, a chain link, a
|
|
5
|
+
* fence, whose shape is in its alpha and not in its geometry. The same test the colour pass makes
|
|
6
|
+
* when it discards, so a surface that is a hole in the picture is a hole in its shadow. Written once,
|
|
7
|
+
* here, because two backends deciding it separately is how they come to disagree about a leaf
|
|
8
|
+
* (AGENTS.md, 2026-08-13).
|
|
9
|
+
*
|
|
10
|
+
* Called per draw, so the answer is written into one object and returned again: a draw allocates
|
|
11
|
+
* nothing. What that gives up is holding two answers at once, which no caller does; the depth pass
|
|
12
|
+
* reads it and binds before it asks again.
|
|
13
|
+
*/
|
|
14
|
+
import type { SurfaceMaterial } from './surfaceTexture.ts';
|
|
15
|
+
|
|
16
|
+
/** A cutout caster's texture, its cutoff and its UV scale. */
|
|
17
|
+
export interface CutoutCaster<Texture> {
|
|
18
|
+
readonly albedo: Texture;
|
|
19
|
+
readonly cutoff: number;
|
|
20
|
+
readonly u: number;
|
|
21
|
+
readonly v: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const answer: { albedo: unknown; cutoff: number; u: number; v: number } = {
|
|
25
|
+
albedo: null,
|
|
26
|
+
cutoff: 0,
|
|
27
|
+
u: 1,
|
|
28
|
+
v: 1,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** The cutout `material` casts, or null for an ordinary caster. */
|
|
32
|
+
export function cutoutOf<Texture>(
|
|
33
|
+
material: SurfaceMaterial<Texture> | null | undefined,
|
|
34
|
+
): CutoutCaster<Texture> | null {
|
|
35
|
+
if (material === null || material === undefined) return null;
|
|
36
|
+
const albedo = material.albedo ?? null;
|
|
37
|
+
const cutoff = material.cutout ?? 0;
|
|
38
|
+
if (albedo === null || !(cutoff > 0)) return null;
|
|
39
|
+
answer.albedo = albedo;
|
|
40
|
+
answer.cutoff = cutoff;
|
|
41
|
+
answer.u = material.uScale ?? 1;
|
|
42
|
+
answer.v = material.vScale ?? 1;
|
|
43
|
+
return answer as CutoutCaster<Texture>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One brick of DriftLight's volume: at every sample, the light that arrives from every light that
|
|
3
|
+
* reaches it, and the direction it mostly arrives from.
|
|
4
|
+
*
|
|
5
|
+
* **Two texels a sample.** The first is the summed colour, and in its fourth channel whether the
|
|
6
|
+
* sample is a sample at all: one standing inside solid geometry is not, and its zero lets the
|
|
7
|
+
* shader's filtered lookup leave it out rather than blend darkness into the wall beside it. The
|
|
8
|
+
* second is the arrival direction, weighted by each light's brightness, so it points at a lone light
|
|
9
|
+
* with a length of one and shrinks towards zero as lights surround the sample from all sides. The
|
|
10
|
+
* shader turns the pair back into the light on a surface of any facing: exactly Lambert for a single
|
|
11
|
+
* light, a quarter of the sum, the mean of a clamped cosine, for light from everywhere.
|
|
12
|
+
*
|
|
13
|
+
* **What it gives up is the specular.** Two numbers of direction cannot say where a highlight
|
|
14
|
+
* falls, which is why the exact lights near the camera are shaded exactly and this stands in only
|
|
15
|
+
* past them, where a highlight from a candle is a fraction of a pixel.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { PointLightFalloff } from './falloff.ts';
|
|
19
|
+
import { pointLightShape } from './falloff.ts';
|
|
20
|
+
import type { LightFieldLayout, LightReach } from './layout.ts';
|
|
21
|
+
import { BRICK_SAMPLES } from './layout.ts';
|
|
22
|
+
import type { DistanceAt } from './visibility.ts';
|
|
23
|
+
import { softVisibility } from './visibility.ts';
|
|
24
|
+
|
|
25
|
+
/** Samples in a brick, and so texels in each of its two blocks. */
|
|
26
|
+
export const BRICK_TEXELS = BRICK_SAMPLES * BRICK_SAMPLES * BRICK_SAMPLES;
|
|
27
|
+
|
|
28
|
+
/** What the bake reads of a light, which a `PointLightSource` already carries. */
|
|
29
|
+
export interface FieldLight extends LightReach {
|
|
30
|
+
readonly r: number;
|
|
31
|
+
readonly g: number;
|
|
32
|
+
readonly b: number;
|
|
33
|
+
/** The emitter's physical radius, which widens its penumbra. */
|
|
34
|
+
readonly sourceRadius: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fill `light` and `direction`, `BRICK_TEXELS * 4` floats each, x fastest, for brick `brick`.
|
|
39
|
+
* `distance` is the scene's distance field, or null for no occlusion.
|
|
40
|
+
*/
|
|
41
|
+
export function bakeBrick(
|
|
42
|
+
layout: LightFieldLayout,
|
|
43
|
+
brick: number,
|
|
44
|
+
lights: readonly FieldLight[],
|
|
45
|
+
falloff: PointLightFalloff,
|
|
46
|
+
distance: DistanceAt | null,
|
|
47
|
+
light: Float32Array,
|
|
48
|
+
direction: Float32Array,
|
|
49
|
+
): void {
|
|
50
|
+
const { spacing, span, origin } = layout;
|
|
51
|
+
const bx = origin[0] + (layout.brickCoords[brick * 3] as number) * span;
|
|
52
|
+
const by = origin[1] + (layout.brickCoords[brick * 3 + 1] as number) * span;
|
|
53
|
+
const bz = origin[2] + (layout.brickCoords[brick * 3 + 2] as number) * span;
|
|
54
|
+
const first = layout.lightStart[brick] as number;
|
|
55
|
+
const last = layout.lightStart[brick + 1] as number;
|
|
56
|
+
|
|
57
|
+
for (let k = 0; k < BRICK_SAMPLES; k++) {
|
|
58
|
+
for (let j = 0; j < BRICK_SAMPLES; j++) {
|
|
59
|
+
for (let i = 0; i < BRICK_SAMPLES; i++) {
|
|
60
|
+
const texel = (i + BRICK_SAMPLES * (j + BRICK_SAMPLES * k)) * 4;
|
|
61
|
+
const px = bx + i * spacing;
|
|
62
|
+
const py = by + j * spacing;
|
|
63
|
+
const pz = bz + k * spacing;
|
|
64
|
+
light.fill(0, texel, texel + 4);
|
|
65
|
+
direction.fill(0, texel, texel + 4);
|
|
66
|
+
if (distance !== null && distance(px, py, pz) < 0) continue;
|
|
67
|
+
|
|
68
|
+
let r = 0;
|
|
69
|
+
let g = 0;
|
|
70
|
+
let b = 0;
|
|
71
|
+
let towardX = 0;
|
|
72
|
+
let towardY = 0;
|
|
73
|
+
let towardZ = 0;
|
|
74
|
+
let weight = 0;
|
|
75
|
+
for (let listed = first; listed < last; listed++) {
|
|
76
|
+
const source = lights[layout.lights[listed] as number];
|
|
77
|
+
if (source === undefined) continue;
|
|
78
|
+
const dx = source.x - px;
|
|
79
|
+
const dy = source.y - py;
|
|
80
|
+
const dz = source.z - pz;
|
|
81
|
+
const dist = Math.hypot(dx, dy, dz);
|
|
82
|
+
if (dist >= source.radius) continue;
|
|
83
|
+
const shape = pointLightShape(dist, source.radius, falloff);
|
|
84
|
+
if (shape <= 0) continue;
|
|
85
|
+
const seen =
|
|
86
|
+
distance === null
|
|
87
|
+
? 1
|
|
88
|
+
: softVisibility(
|
|
89
|
+
distance,
|
|
90
|
+
px,
|
|
91
|
+
py,
|
|
92
|
+
pz,
|
|
93
|
+
source.x,
|
|
94
|
+
source.y,
|
|
95
|
+
source.z,
|
|
96
|
+
source.sourceRadius,
|
|
97
|
+
);
|
|
98
|
+
if (seen <= 0) continue;
|
|
99
|
+
const lr = source.r * shape * seen;
|
|
100
|
+
const lg = source.g * shape * seen;
|
|
101
|
+
const lb = source.b * shape * seen;
|
|
102
|
+
r += lr;
|
|
103
|
+
g += lg;
|
|
104
|
+
b += lb;
|
|
105
|
+
const bright = 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
106
|
+
if (dist > 1e-6 && bright > 0) {
|
|
107
|
+
towardX += (bright * dx) / dist;
|
|
108
|
+
towardY += (bright * dy) / dist;
|
|
109
|
+
towardZ += (bright * dz) / dist;
|
|
110
|
+
weight += bright;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
light[texel] = r;
|
|
114
|
+
light[texel + 1] = g;
|
|
115
|
+
light[texel + 2] = b;
|
|
116
|
+
light[texel + 3] = 1;
|
|
117
|
+
if (weight > 0) {
|
|
118
|
+
direction[texel] = towardX / weight;
|
|
119
|
+
direction[texel + 1] = towardY / weight;
|
|
120
|
+
direction[texel + 2] = towardZ / weight;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a point light's brightness falls with distance, as the lit shader draws it, for the light
|
|
3
|
+
* DriftLight sums into its volume.
|
|
4
|
+
*
|
|
5
|
+
* **One formula in two languages, tied by a test.** The frame shades a light in GLSL; the volume is
|
|
6
|
+
* summed in TypeScript. If they disagree the summed light and the exact light differ where they
|
|
7
|
+
* crossfade, and a seam follows the camera across every surface. `falloff.test.ts` reads the
|
|
8
|
+
* shader's lines and asserts these numbers against them.
|
|
9
|
+
*
|
|
10
|
+
* Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
|
|
11
|
+
* the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
|
|
12
|
+
* floored at ten centimetres so a surface touching a light is not infinitely bright.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type PointLightFalloff = 'smooth' | 'inverseSquare';
|
|
16
|
+
|
|
17
|
+
/** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
|
|
18
|
+
export function pointLightShape(dist: number, radius: number, falloff: PointLightFalloff): number {
|
|
19
|
+
if (falloff === 'inverseSquare') {
|
|
20
|
+
const ratio = dist / Math.max(radius, 1e-4);
|
|
21
|
+
const window = Math.min(1, Math.max(0, 1 - ratio ** 4));
|
|
22
|
+
return (window * window) / Math.max(dist * dist, 0.01);
|
|
23
|
+
}
|
|
24
|
+
const linear = Math.min(1, Math.max(0, 1 - dist / radius));
|
|
25
|
+
return linear * linear;
|
|
26
|
+
}
|