@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mat4 } from 'gl-matrix';
|
|
2
2
|
import type { ProbeBakeOptions } from '../../reflectionProbe.ts';
|
|
3
|
+
import { probeFaceRange } from '../../reflectionProbe.ts';
|
|
3
4
|
import {
|
|
4
5
|
TEMPORAL_HISTORY_BLEND,
|
|
5
6
|
TemporalHistory,
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
BLOOM_FILTER_RADIUS_UV,
|
|
56
57
|
BLOOM_LEVELS,
|
|
57
58
|
bloomLevelSizes,
|
|
59
|
+
bloomThresholdOf,
|
|
58
60
|
bloomProfileWarning,
|
|
59
61
|
} from '../../bloomChain.ts';
|
|
60
62
|
import {
|
|
@@ -93,6 +95,7 @@ import {
|
|
|
93
95
|
sameGrid,
|
|
94
96
|
} from '../../probeGrid.ts';
|
|
95
97
|
import type { ProbeGridOptions } from '../../probeGrid.ts';
|
|
98
|
+
import { ProbeSweeps } from '../../probeSweeps.ts';
|
|
96
99
|
import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.ts';
|
|
97
100
|
import { environmentTexels } from './environmentTexels.ts';
|
|
98
101
|
import { packIesAtlas } from '../../iesProfile.ts';
|
|
@@ -105,7 +108,6 @@ import {
|
|
|
105
108
|
DEFAULT_SOURCE_RADIUS,
|
|
106
109
|
FACE_COUNT,
|
|
107
110
|
createResolvedPointShadows,
|
|
108
|
-
type ResolvedPointShadows,
|
|
109
111
|
} from '../../pointShadowImage.ts';
|
|
110
112
|
import { MAX_AREA_LIGHTS, type AreaLightSource } from '../../areaLights.ts';
|
|
111
113
|
import {
|
|
@@ -178,6 +180,10 @@ import type { RenderBackend } from '../api.ts';
|
|
|
178
180
|
import { reflectionTargetSize, type ReflectionSize } from '../../planarReflectionDraw.ts';
|
|
179
181
|
import { drawingBufferSize } from '../../drawingBuffer.ts';
|
|
180
182
|
import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.ts';
|
|
183
|
+
import { ExposurePass } from './exposurePass.ts';
|
|
184
|
+
import { AO_STORE } from '../../shaders/ambientOcclusion.ts';
|
|
185
|
+
import { clampAutoExposure } from '../../shaders/exposure.ts';
|
|
186
|
+
import { clampLocalExposure } from '../../shaders/localExposure.ts';
|
|
181
187
|
import type { UniformFields } from './scatterPass.ts';
|
|
182
188
|
import {
|
|
183
189
|
createDepthResolveLayout,
|
|
@@ -253,12 +259,15 @@ import {
|
|
|
253
259
|
identityGradeLut,
|
|
254
260
|
validateGradeLut,
|
|
255
261
|
} from '../../colourGrade.ts';
|
|
262
|
+
import {
|
|
263
|
+
FILM_LOOK_WITHOUT_COMPOSITE,
|
|
264
|
+
clampGrain,
|
|
265
|
+
clampVignette,
|
|
266
|
+
grainSeed,
|
|
267
|
+
} from '../../shaders/filmLook.ts';
|
|
256
268
|
import {
|
|
257
269
|
createProbeMipBindGroupLayout,
|
|
258
270
|
createProbePrefilterBindGroupLayout,
|
|
259
|
-
PREFILTER_SAMPLER_BINDING,
|
|
260
|
-
PREFILTER_TEXTURE_BINDING,
|
|
261
|
-
PREFILTER_UNIFORM_BINDING,
|
|
262
271
|
PREFILTER_UNIFORM_OFFSETS,
|
|
263
272
|
probeLevels,
|
|
264
273
|
probeMipPipeline,
|
|
@@ -339,6 +348,9 @@ import {
|
|
|
339
348
|
DEPTH_FRAG_SIZE,
|
|
340
349
|
DEPTH_FRAG_WGSL,
|
|
341
350
|
DEPTH_PREVIOUS_BINDING,
|
|
351
|
+
DEPTH_CUTOUT_VERT_FIELDS,
|
|
352
|
+
DEPTH_CUTOUT_VERT_SIZE,
|
|
353
|
+
DEPTH_INSTANCED_CUTOUT_VERT_FIELDS,
|
|
342
354
|
DEPTH_VERT_FIELDS,
|
|
343
355
|
DEPTH_VERT_SIZE,
|
|
344
356
|
SHADOW_FORMAT,
|
|
@@ -394,7 +406,6 @@ import {
|
|
|
394
406
|
} from './plumePass.ts';
|
|
395
407
|
import {
|
|
396
408
|
PARTICLE_VERT_FIELDS,
|
|
397
|
-
PARTICLE_VERT_SIZE,
|
|
398
409
|
createGpuParticles,
|
|
399
410
|
createParticleBindGroupLayout,
|
|
400
411
|
packInstances,
|
|
@@ -461,8 +472,12 @@ import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
|
|
|
461
472
|
import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.ts';
|
|
462
473
|
import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.ts';
|
|
463
474
|
import { DistanceFieldScene } from '../../gi/fieldScene.ts';
|
|
464
|
-
import { DEFAULT_FIELD_COMPOSE, FieldComposer } from './fieldCompose.ts';
|
|
475
|
+
import { DEFAULT_FIELD_COMPOSE, FieldComposer, MAX_FIELD_CASCADES } from './fieldCompose.ts';
|
|
476
|
+
import { cascadesForGrid } from '../../gi/fieldReach.ts';
|
|
465
477
|
import { ProbeBaker } from './probeBake.ts';
|
|
478
|
+
import type { ProbeBakeLightField } from './probeBake.ts';
|
|
479
|
+
import { createBounceLights, resolveBounceLights } from '../../gi/bounceLights.ts';
|
|
480
|
+
import { ProbeTargets } from './probeTargets.ts';
|
|
466
481
|
import { distanceFieldBounds } from '../../gi/fieldScene.ts';
|
|
467
482
|
import { fitProbeGrid } from '../../gi/probeGridFit.ts';
|
|
468
483
|
|
|
@@ -535,6 +550,11 @@ import type {
|
|
|
535
550
|
import type { Arena, FlushSchedule, ScheduledPass } from '../../frame/index.ts';
|
|
536
551
|
import { GpuInstancedBatch } from './instanced.ts';
|
|
537
552
|
import type { MeshInstances } from '../../instances.ts';
|
|
553
|
+
import { cutoutOf } from '../../cutoutCaster.ts';
|
|
554
|
+
import type { CutoutCaster } from '../../cutoutCaster.ts';
|
|
555
|
+
import { LightField } from '../../driftLight/lightField.ts';
|
|
556
|
+
import type { LightFieldOptions, LightFieldSource } from '../../driftLight/lightField.ts';
|
|
557
|
+
import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.ts';
|
|
538
558
|
|
|
539
559
|
/**
|
|
540
560
|
* How many draws one frame may make before the ring is full.
|
|
@@ -588,6 +608,12 @@ const MAX_SCATTER_DEPTH_DRAWS = 256;
|
|
|
588
608
|
* against the world.
|
|
589
609
|
*/
|
|
590
610
|
const MAX_OVERLAYS = 64;
|
|
611
|
+
/**
|
|
612
|
+
* Sky draws a frame can hold. A frame draws one, a mirror adds one, and a probe bake six a probe —
|
|
613
|
+
* given back after each probe's submit, so a whole grid needs six. Sixty-four is room for insets
|
|
614
|
+
* and passes nobody has written yet; the cost is sixteen kilobytes.
|
|
615
|
+
*/
|
|
616
|
+
const MAX_SKY_DRAWS = 64;
|
|
591
617
|
|
|
592
618
|
/**
|
|
593
619
|
* Distinct materials a frame may set.
|
|
@@ -1213,8 +1239,6 @@ type ShadowLayer = 'static' | 'static-peel' | 'dynamic';
|
|
|
1213
1239
|
|
|
1214
1240
|
/** The tint a draw gets when it asks for none, matching the WebGL2 path's reset-to-white. */
|
|
1215
1241
|
const WHITE = new Float32Array([1, 1, 1]);
|
|
1216
|
-
/** No surface-texture scaling, which is what an untextured mesh wants. */
|
|
1217
|
-
const UNIT_UV = new Float32Array([1, 1]);
|
|
1218
1242
|
/** A dust field standing still, for a light volume that asked for dust and not for wind. */
|
|
1219
1243
|
const NO_DRIFT = new Float32Array([0, 0, 0]);
|
|
1220
1244
|
/**
|
|
@@ -1265,6 +1289,24 @@ const NO_CLIP_PLANE = new Float32Array(4);
|
|
|
1265
1289
|
*/
|
|
1266
1290
|
const MOTION_CLIP_FLIP = mat4.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
1267
1291
|
|
|
1292
|
+
/* Scratch for the probe blur's and convolution's uniform writes, so a bake writes no array literal. */
|
|
1293
|
+
const SCRATCH_ONE = new Float32Array(1);
|
|
1294
|
+
const SCRATCH_VEC4 = new Float32Array(4);
|
|
1295
|
+
const ZERO4 = new Float32Array(4);
|
|
1296
|
+
/** `value` in a one-float scratch, overwritten by the next call. */
|
|
1297
|
+
function one(value: number): Float32Array {
|
|
1298
|
+
SCRATCH_ONE[0] = value;
|
|
1299
|
+
return SCRATCH_ONE;
|
|
1300
|
+
}
|
|
1301
|
+
/** A direction padded to a `vec4`, in a scratch overwritten by the next call. */
|
|
1302
|
+
function padded(direction: ArrayLike<number>): Float32Array {
|
|
1303
|
+
SCRATCH_VEC4[0] = direction[0] ?? 0;
|
|
1304
|
+
SCRATCH_VEC4[1] = direction[1] ?? 0;
|
|
1305
|
+
SCRATCH_VEC4[2] = direction[2] ?? 0;
|
|
1306
|
+
SCRATCH_VEC4[3] = 0;
|
|
1307
|
+
return SCRATCH_VEC4;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1268
1310
|
export class WebGPURenderer implements RendererApi {
|
|
1269
1311
|
/**
|
|
1270
1312
|
* Real timestamp queries, where the adapter offers them.
|
|
@@ -2081,13 +2123,21 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2081
2123
|
private readonly frameEye = new Float32Array(3);
|
|
2082
2124
|
/** The environment the frame was drawn with. See `bindMeshPass`. */
|
|
2083
2125
|
private frameEnv: Environment | null = null;
|
|
2126
|
+
/**
|
|
2127
|
+
* The sky this frame drew, or null until it draws one. A traced probe's escaping rays read it,
|
|
2128
|
+
* so a frame that stops drawing a sky must not go on lighting its probes with the last one.
|
|
2129
|
+
*/
|
|
2130
|
+
private frameSky: SkyColors | null = null;
|
|
2084
2131
|
/** That matrix with the clip-space correction folded in, rebuilt once a frame. */
|
|
2085
2132
|
private readonly correctedViewProj = new Float32Array(16);
|
|
2086
2133
|
|
|
2087
2134
|
private readonly skyLayout: GPUBindGroupLayout;
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2135
|
+
/**
|
|
2136
|
+
* A slot a sky draw, as every per-draw resource here is: see `UniformRing` and the 2026-08-27 rule.
|
|
2137
|
+
* It was one buffer rewritten at each draw, which gave every sky in an encoder the last camera.
|
|
2138
|
+
*/
|
|
2139
|
+
private readonly skyUniforms: UniformRing;
|
|
2140
|
+
private readonly skyFloats = new Float32Array(SKY_UNIFORM_SIZE / 4);
|
|
2091
2141
|
private readonly skyBindGroup: GPUBindGroup;
|
|
2092
2142
|
/** The camera's inverse with the clip correction undone, rebuilt once a frame. */
|
|
2093
2143
|
private readonly skyInvViewProj = new Float32Array(16);
|
|
@@ -2363,35 +2413,74 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2363
2413
|
* prefix that has already been uploaded costs one write of unchanged bytes and cannot be
|
|
2364
2414
|
* wrong: a ring only ever appends within a frame.
|
|
2365
2415
|
*/
|
|
2416
|
+
/**
|
|
2417
|
+
* Every per-draw ring the frame writes and flushes together, in one list.
|
|
2418
|
+
*
|
|
2419
|
+
* **One list, read by the flush and by the probe bake's rewind**, so a ring added to one cannot be
|
|
2420
|
+
* forgotten by the other: a ring flushed but never rewound would let a grid bake spend it, and one
|
|
2421
|
+
* rewound but never flushed would upload nothing. Built on first use, after the constructor has
|
|
2422
|
+
* set every field; `growTo` replaces a ring's buffer, never the ring, so the list stays true.
|
|
2423
|
+
*/
|
|
2424
|
+
private ringList: readonly UniformRing[] | null = null;
|
|
2425
|
+
private rings(): readonly UniformRing[] {
|
|
2426
|
+
this.ringList ??= [
|
|
2427
|
+
this.perDraw,
|
|
2428
|
+
this.lightVolumeVertices,
|
|
2429
|
+
this.lightVolumeFragments,
|
|
2430
|
+
this.plumeVerts,
|
|
2431
|
+
this.plumeFrags,
|
|
2432
|
+
this.waterVerts,
|
|
2433
|
+
this.waterFrags,
|
|
2434
|
+
this.boltVerts,
|
|
2435
|
+
this.boltFrags,
|
|
2436
|
+
this.causticsVerts,
|
|
2437
|
+
this.causticsFrags,
|
|
2438
|
+
this.flockVerts,
|
|
2439
|
+
this.flockFrags,
|
|
2440
|
+
this.windStreakVerts,
|
|
2441
|
+
this.windStreakFrags,
|
|
2442
|
+
this.scatterVerts,
|
|
2443
|
+
this.scatterFrags,
|
|
2444
|
+
this.perFrame,
|
|
2445
|
+
this.filmVerts,
|
|
2446
|
+
this.filmFrags,
|
|
2447
|
+
this.insetUniforms,
|
|
2448
|
+
this.panelVerts,
|
|
2449
|
+
this.panelFrags,
|
|
2450
|
+
this.textVerts,
|
|
2451
|
+
this.textFrags,
|
|
2452
|
+
this.sdfTextVerts,
|
|
2453
|
+
this.sdfTextFrags,
|
|
2454
|
+
this.lineVerts,
|
|
2455
|
+
this.lineFrags,
|
|
2456
|
+
this.skyUniforms,
|
|
2457
|
+
];
|
|
2458
|
+
return this.ringList;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2366
2461
|
private flushRings(): void {
|
|
2367
|
-
this.
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
this.
|
|
2374
|
-
this.
|
|
2375
|
-
this.
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
this.
|
|
2386
|
-
|
|
2387
|
-
this.
|
|
2388
|
-
this.
|
|
2389
|
-
this.textVerts.flush();
|
|
2390
|
-
this.textFrags.flush();
|
|
2391
|
-
this.sdfTextVerts.flush();
|
|
2392
|
-
this.sdfTextFrags.flush();
|
|
2393
|
-
this.lineVerts.flush();
|
|
2394
|
-
this.lineFrags.flush();
|
|
2462
|
+
for (const ring of this.rings()) ring.flush();
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
/** Where every ring stands before work that submits an encoder of its own. */
|
|
2466
|
+
private readonly ringMarks: number[] = [];
|
|
2467
|
+
private markRings(): void {
|
|
2468
|
+
const rings = this.rings();
|
|
2469
|
+
this.ringMarks.length = rings.length;
|
|
2470
|
+
for (let i = 0; i < rings.length; i++) this.ringMarks[i] = (rings[i] as UniformRing).mark();
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
/**
|
|
2474
|
+
* Give back every slot taken since `markRings`, once that work has been submitted.
|
|
2475
|
+
*
|
|
2476
|
+
* See `UniformRing.rewind` for why this is safe only after the submit. The palette ring goes back
|
|
2477
|
+
* with them, and the cached palette slot is dropped if it was one of those given back.
|
|
2478
|
+
*/
|
|
2479
|
+
private rewindRings(skinMark: number): void {
|
|
2480
|
+
const rings = this.rings();
|
|
2481
|
+
for (let i = 0; i < rings.length; i++) (rings[i] as UniformRing).rewind(this.ringMarks[i] ?? 0);
|
|
2482
|
+
this.skinPalettes.rewind(skinMark);
|
|
2483
|
+
if (this.skinPaletteSlot >= skinMark) this.skinPaletteSlot = -1;
|
|
2395
2484
|
}
|
|
2396
2485
|
|
|
2397
2486
|
/* -- The film ------------------------------------------------------------------------- */
|
|
@@ -2600,6 +2689,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2600
2689
|
private blankAlbedoBindGroup: GPUBindGroup;
|
|
2601
2690
|
/** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
|
|
2602
2691
|
private readonly uvScale = new Float32Array([1, 1]);
|
|
2692
|
+
/** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
|
|
2693
|
+
private materialDoubleSided = false;
|
|
2603
2694
|
|
|
2604
2695
|
createSurfaceTexture(
|
|
2605
2696
|
source: TexImageSource,
|
|
@@ -2664,6 +2755,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2664
2755
|
* a group holding the old view hands a destroyed texture to the next draw that asks.
|
|
2665
2756
|
*/
|
|
2666
2757
|
private forgetBindingsOf(texture: GpuSurfaceTexture): void {
|
|
2758
|
+
this.cutoutGroups.delete(texture);
|
|
2759
|
+
this.cutoutPeelGroups.delete(texture);
|
|
2667
2760
|
/* Same reasoning, for whichever SDF labels were bound against this atlas. */
|
|
2668
2761
|
this.sdfTextBindGroups.delete(texture);
|
|
2669
2762
|
this.flatBindGroups.clear();
|
|
@@ -2720,12 +2813,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2720
2813
|
const albedo = material?.albedo ?? null;
|
|
2721
2814
|
this.uvScale[0] = material?.uScale ?? 1;
|
|
2722
2815
|
this.uvScale[1] = material?.vScale ?? 1;
|
|
2816
|
+
this.materialDoubleSided = material?.doubleSided === true;
|
|
2723
2817
|
const normal = (material?.normal ?? null) as GpuSurfaceTexture | null;
|
|
2724
2818
|
const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
|
|
2725
2819
|
const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
|
|
2726
2820
|
this.material((f, i) => {
|
|
2727
2821
|
i[this.materialField('uAlbedoEnabled')] = albedo === null ? 0 : 1;
|
|
2728
2822
|
f[this.materialField('uAlbedoCutout')] = material?.cutout ?? 0;
|
|
2823
|
+
i[this.materialField('uDoubleSided')] = material?.doubleSided === true ? 1 : 0;
|
|
2729
2824
|
/* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
|
|
2730
2825
|
f[this.materialField('uNormalStrength')] =
|
|
2731
2826
|
normal === null ? 0 : (material?.normalStrength ?? 1);
|
|
@@ -3586,6 +3681,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3586
3681
|
timeSeconds: number,
|
|
3587
3682
|
): void {
|
|
3588
3683
|
if (!this.canDraw() || data.count === 0) return;
|
|
3684
|
+
if (batch.drawnInEncoder === this.encoderEpoch) {
|
|
3685
|
+
if (!this.warnedParticlesTwice) {
|
|
3686
|
+
this.warnedParticlesTwice = true;
|
|
3687
|
+
console.warn(
|
|
3688
|
+
'WebGPU: a particle batch was drawn twice into one encoder — a mirror and the view, or a ' +
|
|
3689
|
+
"probe's faces — and the second draw is skipped, because both would show the second " +
|
|
3690
|
+
"draw's particles. Give each view its own batch.",
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
batch.drawnInEncoder = this.encoderEpoch;
|
|
3589
3696
|
const live = packInstances(batch, data);
|
|
3590
3697
|
if (live === 0) return;
|
|
3591
3698
|
|
|
@@ -4364,7 +4471,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4364
4471
|
|
|
4365
4472
|
this.windStreakFragFloats.set(tint, (WIND_STREAK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
|
|
4366
4473
|
|
|
4367
|
-
const { queue } = this.surface.device;
|
|
4368
4474
|
this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
|
|
4369
4475
|
this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
|
|
4370
4476
|
|
|
@@ -4466,7 +4572,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4466
4572
|
|
|
4467
4573
|
this.flockFragFloats.set(tint, (FLOCK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
|
|
4468
4574
|
|
|
4469
|
-
const { queue } = this.surface.device;
|
|
4470
4575
|
this.flockVerts.writeBlock(vertexSlot, this.flockVertBlock);
|
|
4471
4576
|
this.flockFrags.writeBlock(fragmentSlot, this.flockFragBlock);
|
|
4472
4577
|
|
|
@@ -4819,7 +4924,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4819
4924
|
private readonly scatterLayout: GPUBindGroupLayout;
|
|
4820
4925
|
/** One slot per batch: the gust and the trample field belong to the batch, not the frame. */
|
|
4821
4926
|
private readonly scatterVerts: UniformRing;
|
|
4822
|
-
|
|
4927
|
+
/** A slot a draw, beside the vertex block's: see `createScatterBindGroupLayout` for why. */
|
|
4928
|
+
private readonly scatterFrags: UniformRing;
|
|
4823
4929
|
private readonly scatterFragStaging = new ArrayBuffer(SCATTER_FRAG_SIZE);
|
|
4824
4930
|
private readonly scatterFragFloats = new Float32Array(this.scatterFragStaging);
|
|
4825
4931
|
private readonly scatterFragInts = new Int32Array(this.scatterFragStaging);
|
|
@@ -5014,17 +5120,28 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5014
5120
|
private readonly dynamicView: GPUTextureView | null;
|
|
5015
5121
|
/** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
|
|
5016
5122
|
private readonly shadowPeelBindGroup: GPUBindGroup | null;
|
|
5123
|
+
/**
|
|
5124
|
+
* The cutout casters' layout, and a bind group per cutout map for each layer, built the first
|
|
5125
|
+
* time a map casts and kept: a `createBindGroup` per draw is the allocation the house rules forbid.
|
|
5126
|
+
* Cleared in `forgetBindingsOf`, because a group outliving its texture is a destroyed-texture error
|
|
5127
|
+
* at submit, and a texture's view is new when an update changes its size.
|
|
5128
|
+
*/
|
|
5129
|
+
private readonly shadowCutoutLayout: GPUBindGroupLayout;
|
|
5130
|
+
private readonly cutoutGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
|
|
5131
|
+
private readonly cutoutPeelGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
|
|
5132
|
+
private shadowPeelPassUniforms: GPUBuffer | null = null;
|
|
5133
|
+
private shadowPeelSampler: GPUSampler | null = null;
|
|
5017
5134
|
private readonly scatterDepthPeelBindGroup: GPUBindGroup | null;
|
|
5018
5135
|
/** Which layer the open pass is writing, so the sink picks the bindings that match it. */
|
|
5019
5136
|
private shadowLayerIsPeel = false;
|
|
5020
5137
|
/**
|
|
5021
5138
|
* Whether a peel pass has filled the map, which is what the frame may sample it on.
|
|
5022
5139
|
*
|
|
5023
|
-
* **Not whether the map exists.** `renderer.ts`
|
|
5024
|
-
* born
|
|
5025
|
-
*
|
|
5026
|
-
* static layer opens and set when a peel pass ends, which
|
|
5027
|
-
*
|
|
5140
|
+
* **Not whether the map exists.** `renderer.ts` asked that question, on the belief that a GL
|
|
5141
|
+
* texture is born reading as the far plane, and it is born zeroed: a scene that built a peel
|
|
5142
|
+
* map and never opened the layer had no sun at all under ANGLE's GL backend. It holds this
|
|
5143
|
+
* rule now as well. Cleared when the static layer opens and set when a peel pass ends, which
|
|
5144
|
+
* is per frame because every scene that peels does so every frame.
|
|
5028
5145
|
*/
|
|
5029
5146
|
private peelFilled = false;
|
|
5030
5147
|
/** One slot per caster, the same trick `perDraw` uses and for the same reason. */
|
|
@@ -5066,6 +5183,44 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5066
5183
|
* otherwise. A lookup rather than a rebind, because a `createBindGroup` per draw is the
|
|
5067
5184
|
* per-frame allocation the house rules forbid; both groups are built once at init.
|
|
5068
5185
|
*/
|
|
5186
|
+
/** The cutout bind group for `texture` in the open layer, built the first time it casts there. */
|
|
5187
|
+
private cutoutGroup(texture: GpuSurfaceTexture): GPUBindGroup | null {
|
|
5188
|
+
const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
|
|
5189
|
+
const groups = peel ? this.cutoutPeelGroups : this.cutoutGroups;
|
|
5190
|
+
const held = groups.get(texture);
|
|
5191
|
+
if (held !== undefined) return held;
|
|
5192
|
+
const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
|
|
5193
|
+
const previousView = peel ? this.shadowView : this.shadowBlankView;
|
|
5194
|
+
const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
|
|
5195
|
+
if (passUniforms === null || previousView === null || previousSampler === null) return null;
|
|
5196
|
+
const group = createDepthBindGroup(
|
|
5197
|
+
this.surface.device,
|
|
5198
|
+
this.shadowCutoutLayout,
|
|
5199
|
+
this.shadowDraws.buffer,
|
|
5200
|
+
passUniforms,
|
|
5201
|
+
previousView,
|
|
5202
|
+
previousSampler,
|
|
5203
|
+
null,
|
|
5204
|
+
{ view: texture.view, sampler: texture.sampler },
|
|
5205
|
+
);
|
|
5206
|
+
groups.set(texture, group);
|
|
5207
|
+
return group;
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
/** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
|
|
5211
|
+
private writeCutout(
|
|
5212
|
+
slot: number,
|
|
5213
|
+
cutout: CutoutCaster<unknown>,
|
|
5214
|
+
fields: {
|
|
5215
|
+
readonly uUvScale: { readonly offset: number };
|
|
5216
|
+
readonly uAlphaCutout: { readonly offset: number };
|
|
5217
|
+
},
|
|
5218
|
+
): void {
|
|
5219
|
+
this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
|
|
5220
|
+
this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
|
|
5221
|
+
this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5069
5224
|
private shadowGroup(): GPUBindGroup {
|
|
5070
5225
|
return this.shadowLayerIsPeel && this.shadowPeelBindGroup !== null
|
|
5071
5226
|
? this.shadowPeelBindGroup
|
|
@@ -5114,9 +5269,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5114
5269
|
* every frame would be an allocation in a per-frame path.
|
|
5115
5270
|
*/
|
|
5116
5271
|
private readonly casterSink: ShadowCasterSink = {
|
|
5117
|
-
mesh: (mesh, model) => {
|
|
5272
|
+
mesh: (mesh, model, material) => {
|
|
5118
5273
|
const pass = this.shadowPass;
|
|
5119
5274
|
if (pass === null) return;
|
|
5275
|
+
/* A cutout caster's group is looked up before a slot is spent, so a miss costs nothing. */
|
|
5276
|
+
const cutout = cutoutOf(material);
|
|
5277
|
+
const cutoutGroup =
|
|
5278
|
+
cutout === null ? null : this.cutoutGroup(cutout.albedo as unknown as GpuSurfaceTexture);
|
|
5120
5279
|
const slot = this.shadowDraws.allocate();
|
|
5121
5280
|
if (slot === null) return;
|
|
5122
5281
|
|
|
@@ -5127,20 +5286,28 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5127
5286
|
);
|
|
5128
5287
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model as Float32Array);
|
|
5129
5288
|
this.writeShadowWind(slot);
|
|
5289
|
+
if (cutout !== null && cutoutGroup !== null)
|
|
5290
|
+
this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
|
|
5130
5291
|
|
|
5131
5292
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
5132
5293
|
if (geometry.vertexBuffers === undefined) return;
|
|
5294
|
+
const cuts = cutoutGroup !== null;
|
|
5295
|
+
/* A two-sided caster casts from whichever face the light sees: nothing culled. */
|
|
5296
|
+
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
5133
5297
|
pass.setPipeline(
|
|
5134
5298
|
depthPipeline(
|
|
5135
5299
|
this.pipelines,
|
|
5136
5300
|
this.surface.device,
|
|
5137
|
-
this.shadowLayout,
|
|
5138
|
-
|
|
5301
|
+
cuts ? this.shadowCutoutLayout : this.shadowLayout,
|
|
5302
|
+
`${cuts ? 'depth-cutout' : 'depth'}|${cull}|${geometry.key ?? ''}`,
|
|
5139
5303
|
this.presentOf(geometry),
|
|
5140
|
-
|
|
5304
|
+
cull,
|
|
5305
|
+
false,
|
|
5306
|
+
false,
|
|
5307
|
+
cuts,
|
|
5141
5308
|
),
|
|
5142
5309
|
);
|
|
5143
|
-
pass.setBindGroup(0, this.shadowGroup(), [slot]);
|
|
5310
|
+
pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
|
|
5144
5311
|
for (let index = 0; index < geometry.vertexBuffers.length; index++) {
|
|
5145
5312
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
|
|
5146
5313
|
}
|
|
@@ -5151,7 +5318,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5151
5318
|
* A batch of rigid meshes into the depth map, placed by the same matrices the visible draw
|
|
5152
5319
|
* uses — one draw, however many instances, exactly as the colour pass does it.
|
|
5153
5320
|
*/
|
|
5154
|
-
instanced: (batch, data) => {
|
|
5321
|
+
instanced: (batch, data, material) => {
|
|
5155
5322
|
const pass = this.shadowPass;
|
|
5156
5323
|
if (pass === null) return;
|
|
5157
5324
|
const gpuBatch = batch as GpuInstancedBatch;
|
|
@@ -5159,6 +5326,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5159
5326
|
if (geometry.vertexBuffers === undefined) return;
|
|
5160
5327
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
5161
5328
|
if (count === 0) return;
|
|
5329
|
+
const cutout = cutoutOf(material);
|
|
5330
|
+
const cutoutGroup =
|
|
5331
|
+
cutout === null ? null : this.cutoutGroup(cutout.albedo as unknown as GpuSurfaceTexture);
|
|
5162
5332
|
|
|
5163
5333
|
const slot = this.shadowDraws.allocate();
|
|
5164
5334
|
if (slot === null) return;
|
|
@@ -5169,20 +5339,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5169
5339
|
DEPTH_VERT_FIELDS.uLightViewProj.offset,
|
|
5170
5340
|
this.correctedLightViewProj,
|
|
5171
5341
|
);
|
|
5342
|
+
if (cutout !== null && cutoutGroup !== null) {
|
|
5343
|
+
this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
|
|
5344
|
+
}
|
|
5172
5345
|
|
|
5346
|
+
const cuts = cutoutGroup !== null;
|
|
5347
|
+
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
5173
5348
|
pass.setPipeline(
|
|
5174
5349
|
depthPipeline(
|
|
5175
5350
|
this.pipelines,
|
|
5176
5351
|
this.surface.device,
|
|
5177
|
-
this.shadowLayout,
|
|
5178
|
-
|
|
5352
|
+
cuts ? this.shadowCutoutLayout : this.shadowLayout,
|
|
5353
|
+
`${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}|${cull}|${geometry.key ?? ''}`,
|
|
5179
5354
|
this.presentOf(geometry),
|
|
5180
|
-
|
|
5355
|
+
cull,
|
|
5181
5356
|
false,
|
|
5182
5357
|
true,
|
|
5358
|
+
cuts,
|
|
5183
5359
|
),
|
|
5184
5360
|
);
|
|
5185
|
-
pass.setBindGroup(0, this.shadowGroup(), [slot]);
|
|
5361
|
+
pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
|
|
5186
5362
|
let index = 0;
|
|
5187
5363
|
for (; index < geometry.vertexBuffers.length; index++) {
|
|
5188
5364
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
|
|
@@ -5681,34 +5857,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5681
5857
|
const blankView = blank.createView();
|
|
5682
5858
|
const blankSampler = device.createSampler({ label: 'flat.sampler' });
|
|
5683
5859
|
|
|
5684
|
-
/*
|
|
5685
|
-
* A one-texel cube for every point-shadow sampler the variant declares.
|
|
5686
|
-
*
|
|
5687
|
-
* Twelve of them exist in the `pointShadows` permutation and none has a map behind it
|
|
5688
|
-
* until row 3, but WebGPU requires every declared binding to be filled — and a cube view
|
|
5689
|
-
* needs six array layers, so the flat 1×1 stand-in above cannot serve.
|
|
5690
|
-
*
|
|
5691
|
-
* **White is the second guard, and it is a deliberate value rather than a spare one.** The
|
|
5692
|
-
* first is `uPointShadowIndex`, written to −1 for every light in `bindMeshPass`. If that
|
|
5693
|
-
* ever failed, a full-depth texel reads as an occluder at the far plane — `pointShadow`
|
|
5694
|
-
* converts the stored value back to a distance and 1.0 comes out as `far` — so the worst a
|
|
5695
|
-
* mis-selected light can do is find nothing in the way. Zeroes would have meant an
|
|
5696
|
-
* occluder pressed against the lamp, and every surface in the scene in shadow.
|
|
5697
|
-
*/
|
|
5698
|
-
const blankCube = device.createTexture({
|
|
5699
|
-
label: 'flat.emptyPointShadow',
|
|
5700
|
-
size: [1, 1, 6],
|
|
5701
|
-
format: 'rgba8unorm',
|
|
5702
|
-
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
5703
|
-
});
|
|
5704
|
-
device.queue.writeTexture(
|
|
5705
|
-
{ texture: blankCube },
|
|
5706
|
-
new Uint8Array(4 * 6).fill(255),
|
|
5707
|
-
{ bytesPerRow: 4, rowsPerImage: 1 },
|
|
5708
|
-
[1, 1, 6],
|
|
5709
|
-
);
|
|
5710
|
-
const blankCubeView = blankCube.createView({ dimension: 'cube' });
|
|
5711
|
-
|
|
5712
5860
|
/*
|
|
5713
5861
|
* A one-texel integer stand-in for the froxel table.
|
|
5714
5862
|
*
|
|
@@ -5758,6 +5906,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5758
5906
|
[1, 1, 1],
|
|
5759
5907
|
);
|
|
5760
5908
|
const blankArrayView = blankArray.createView({ dimension: '2d-array' });
|
|
5909
|
+
/*
|
|
5910
|
+
* DriftLight's two volumes before a field is whole: an index of one empty cell and an atlas
|
|
5911
|
+
* of one zero texel. Never read while `uDriftLight.x` is zero, and bound anyway, because a
|
|
5912
|
+
* group missing an entry its layout declares is a validation failure at `submit`.
|
|
5913
|
+
*/
|
|
5914
|
+
const blankVolume = (label: string, format: GPUTextureFormat): GPUTextureView =>
|
|
5915
|
+
device
|
|
5916
|
+
.createTexture({ label, size: [1, 1, 1], dimension: '3d', format, usage: 0x2 | 0x4 })
|
|
5917
|
+
.createView({ dimension: '3d' });
|
|
5918
|
+
const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
|
|
5919
|
+
const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
|
|
5761
5920
|
|
|
5762
5921
|
this.shadowMap = device.createTexture({
|
|
5763
5922
|
label: 'shadow.static',
|
|
@@ -5797,14 +5956,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5797
5956
|
this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
|
|
5798
5957
|
|
|
5799
5958
|
/*
|
|
5800
|
-
* **Cleared once, here, because a texture no pass has written
|
|
5959
|
+
* **Cleared once, here, because a texture no pass has written reads as zero**, which the
|
|
5960
|
+
* specification requires, and a zero in a map compared with `LEQUAL` is an occluder at the
|
|
5961
|
+
* light. A scene is entitled to switch directional shadows *on* and never open a pass —
|
|
5962
|
+
* `night-street` does exactly that, with `shadowStrength` at 0.85 and no `beginShadowPass`
|
|
5963
|
+
* anywhere in it — and sampling that zero shadows everything, which is a picture rather than
|
|
5964
|
+
* an error and reads as a lighting bug.
|
|
5801
5965
|
*
|
|
5802
|
-
*
|
|
5803
|
-
*
|
|
5804
|
-
* no such promise, and a scene is entitled to switch directional shadows *on* and never
|
|
5805
|
-
* open a pass — `night-street` does exactly that, with `shadowStrength` at 0.85 and no
|
|
5806
|
-
* `beginShadowPass` anywhere in it. Sampling garbage then shadows whatever the garbage
|
|
5807
|
-
* happens to say, which is a picture rather than an error and reads as a lighting bug.
|
|
5966
|
+
* `shadowMap.ts` does the same for the other backend, for the same reason found later: WebGL
|
|
5967
|
+
* zeroes a new texture too, and only ANGLE on Vulkan happened to hand one back reading 1.
|
|
5808
5968
|
*
|
|
5809
5969
|
* Found by dumping the flat block off both live renderers and comparing it field by field;
|
|
5810
5970
|
* `uPeeledShadowEnabled` came back 1 against 0 and pointed straight at the maps behind it.
|
|
@@ -5914,7 +6074,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5914
6074
|
* is writing, so the binding has to move whatever the shader intends to do with it.
|
|
5915
6075
|
*/
|
|
5916
6076
|
if (name === 'uEnvironment') {
|
|
5917
|
-
return this.probeView === null || !this.
|
|
6077
|
+
return this.probeView === null || !this.probeReadable()
|
|
5918
6078
|
? { view: blankArrayView, sampler: blankSampler }
|
|
5919
6079
|
: { view: this.probeView, sampler: this.probeSampler };
|
|
5920
6080
|
}
|
|
@@ -5948,6 +6108,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5948
6108
|
if (name === 'uCookieAtlas') {
|
|
5949
6109
|
return { view: this.cookieView ?? blankView, sampler: iesSampler };
|
|
5950
6110
|
}
|
|
6111
|
+
/*
|
|
6112
|
+
* DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
|
|
6113
|
+
* sampler the way the froxel table does; and its atlas, which is filtered inside a brick and
|
|
6114
|
+
* so takes the linear, clamped one the photometric atlas uses.
|
|
6115
|
+
*/
|
|
6116
|
+
if (name === 'uDriftLightIndex') {
|
|
6117
|
+
return { view: this.driftIndexView ?? blankDriftIndexView, sampler: shadowSampler };
|
|
6118
|
+
}
|
|
6119
|
+
if (name === 'uDriftLightAtlas') {
|
|
6120
|
+
return { view: this.driftAtlasView ?? blankDriftAtlasView, sampler: iesSampler };
|
|
6121
|
+
}
|
|
5951
6122
|
/*
|
|
5952
6123
|
* The colour the frame had already drawn, or the blank texel a frame that refracts nothing
|
|
5953
6124
|
* falls back to.
|
|
@@ -6003,12 +6174,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6003
6174
|
this.blankAlbedoBindGroup = this.bindGroup;
|
|
6004
6175
|
|
|
6005
6176
|
this.skyLayout = createSkyBindGroupLayout(device);
|
|
6006
|
-
this.skyUniforms =
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6177
|
+
this.skyUniforms = new UniformRing(
|
|
6178
|
+
device,
|
|
6179
|
+
SKY_UNIFORM_SIZE,
|
|
6180
|
+
MAX_SKY_DRAWS,
|
|
6181
|
+
USAGE_UNIFORM_DST,
|
|
6182
|
+
'sky.ring',
|
|
6183
|
+
);
|
|
6184
|
+
this.skyBindGroup = createSkyBindGroup(device, this.skyLayout, this.skyUniforms.buffer);
|
|
6012
6185
|
|
|
6013
6186
|
this.plumeLayout = createPlumeBindGroupLayout(device);
|
|
6014
6187
|
this.plumeVerts = new UniformRing(
|
|
@@ -6087,6 +6260,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6087
6260
|
usage: 0x10 | 0x4 | 0x2, // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_DST
|
|
6088
6261
|
})
|
|
6089
6262
|
: null;
|
|
6263
|
+
this.probeTargets =
|
|
6264
|
+
this.probe === null ? null : new ProbeTargets(device, this.probe, this.probeSize);
|
|
6090
6265
|
/*
|
|
6091
6266
|
* The prefiltered twin, allocated on exactly the condition the capture is and with exactly
|
|
6092
6267
|
* its format and level count. Same format because a pipeline's colour target must equal its
|
|
@@ -6129,6 +6304,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6129
6304
|
is not a screen-sized target. Filtering is what makes a 32-lattice table smooth rather
|
|
6130
6305
|
than banded, so it is declared filterable like the colour inputs and unlike the depth. */
|
|
6131
6306
|
{ binding: RUSH_TEXTURES.uGradeLut, dimension: '3d' },
|
|
6307
|
+
/* Eye adaptation's held brightness: one half-float texel, or the scene where none is held. */
|
|
6308
|
+
{ binding: RUSH_TEXTURES.uExposureHeld },
|
|
6309
|
+
/* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
|
|
6310
|
+
{ binding: RUSH_TEXTURES.uExposureLocal },
|
|
6132
6311
|
]);
|
|
6133
6312
|
this.rushUniforms = device.createBuffer({
|
|
6134
6313
|
label: 'post.rushUniforms',
|
|
@@ -6358,23 +6537,27 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6358
6537
|
MAX_PLUMES_PER_FRAME,
|
|
6359
6538
|
USAGE_UNIFORM_DST,
|
|
6360
6539
|
);
|
|
6361
|
-
this.
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6540
|
+
this.scatterFrags = new UniformRing(
|
|
6541
|
+
device,
|
|
6542
|
+
SCATTER_FRAG_SIZE,
|
|
6543
|
+
MAX_PLUMES_PER_FRAME,
|
|
6544
|
+
USAGE_UNIFORM_DST,
|
|
6545
|
+
'scatter.fragRing',
|
|
6546
|
+
);
|
|
6366
6547
|
this.scatterBindGroup = createScatterBindGroup(
|
|
6367
6548
|
device,
|
|
6368
6549
|
this.scatterLayout,
|
|
6369
6550
|
this.scatterVerts.buffer,
|
|
6370
|
-
this.
|
|
6551
|
+
this.scatterFrags.buffer,
|
|
6371
6552
|
);
|
|
6372
6553
|
|
|
6373
6554
|
this.shadowLayout = createDepthBindGroupLayout(device);
|
|
6374
6555
|
this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
|
|
6556
|
+
this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
|
|
6375
6557
|
this.shadowDraws = new UniformRing(
|
|
6376
6558
|
device,
|
|
6377
|
-
|
|
6559
|
+
/* The cutout block is the plain one and two fields more, and a slot must hold either. */
|
|
6560
|
+
Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE),
|
|
6378
6561
|
MAX_DRAWS_PER_FRAME,
|
|
6379
6562
|
USAGE_UNIFORM_DST,
|
|
6380
6563
|
'shadow.drawRing',
|
|
@@ -6446,6 +6629,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6446
6629
|
if (peelPassUniforms !== null) {
|
|
6447
6630
|
device.queue.writeBuffer(peelPassUniforms, 0, new Int32Array([1]));
|
|
6448
6631
|
}
|
|
6632
|
+
this.shadowPeelPassUniforms = peelPassUniforms;
|
|
6633
|
+
this.shadowPeelSampler = shadowSampler;
|
|
6449
6634
|
this.shadowPeelBindGroup =
|
|
6450
6635
|
peelPassUniforms === null
|
|
6451
6636
|
? null
|
|
@@ -6470,15 +6655,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6470
6655
|
shadowSampler,
|
|
6471
6656
|
);
|
|
6472
6657
|
|
|
6473
|
-
/*
|
|
6474
|
-
* The light volume, last because it wants the shadow map and the stand-in that everything
|
|
6475
|
-
* above already built. The peel and dynamic maps get the white one-pixel texture, which is
|
|
6476
|
-
* depth 1 and therefore nothing in the way — see `createLightVolumeBindGroup` for why a
|
|
6477
|
-
* stand-in of zero would put every beam in shadow instead.
|
|
6478
|
-
*/
|
|
6479
|
-
const uniformBuffer = (label: string, size: number): GPUBuffer =>
|
|
6480
|
-
device.createBuffer({ label, size, usage: USAGE_UNIFORM_DST });
|
|
6481
|
-
|
|
6482
6658
|
/*
|
|
6483
6659
|
* The overlays. Rings rather than single blocks because a frame draws several of each —
|
|
6484
6660
|
* the loader alone is two panels and two strings — and `queue.writeBuffer` does not
|
|
@@ -6801,6 +6977,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6801
6977
|
this.reflectionQueue.reset();
|
|
6802
6978
|
/* Redeclared every frame, so a field a consumer stops declaring stops lighting. */
|
|
6803
6979
|
this.distanceFields.reset();
|
|
6980
|
+
this.frameSky = null;
|
|
6804
6981
|
this.oitActive =
|
|
6805
6982
|
this.quality.orderIndependent && this.quality.screenEffects && this.samples === 1;
|
|
6806
6983
|
/* Read off the line above rather than restating its last term: with the other two true, the
|
|
@@ -6823,6 +7000,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6823
7000
|
this.flushOverlay();
|
|
6824
7001
|
|
|
6825
7002
|
this.framePresented = false;
|
|
7003
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
6826
7004
|
/* Before the budget is cleared, because what to grow to is what the last frame asked for. */
|
|
6827
7005
|
this.growRings();
|
|
6828
7006
|
this.budget.reset();
|
|
@@ -6862,6 +7040,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6862
7040
|
this.windStreakVerts.reset();
|
|
6863
7041
|
this.windStreakFrags.reset();
|
|
6864
7042
|
this.scatterVerts.reset();
|
|
7043
|
+
this.scatterFrags.reset();
|
|
6865
7044
|
this.panelVerts.reset();
|
|
6866
7045
|
this.panelFrags.reset();
|
|
6867
7046
|
this.filmVerts.reset();
|
|
@@ -6873,6 +7052,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6873
7052
|
this.sdfTextFrags.reset();
|
|
6874
7053
|
this.lineVerts.reset();
|
|
6875
7054
|
this.lineFrags.reset();
|
|
7055
|
+
this.skyUniforms.reset();
|
|
6876
7056
|
/* The material ring, and the open slot with it: last frame's slots are gone. */
|
|
6877
7057
|
this.perFrame.reset();
|
|
6878
7058
|
this.materials.dirty();
|
|
@@ -7469,9 +7649,29 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7469
7649
|
f[base + 1] = counts[1];
|
|
7470
7650
|
f[base + 2] = counts[2];
|
|
7471
7651
|
}
|
|
7652
|
+
/* Which sets the shading reads, and how far between them. See `probeSweeps.ts`. */
|
|
7653
|
+
const setsAt = this.fragment.fields['uProbeGridSets'];
|
|
7654
|
+
if (setsAt !== undefined) {
|
|
7655
|
+
const sets = this.probeSweeps?.uniforms(this.probeSets) ?? this.probeSets.fill(0);
|
|
7656
|
+
const base = setsAt.offset / 4;
|
|
7657
|
+
f[base] = sets[0] ?? 0;
|
|
7658
|
+
f[base + 1] = sets[1] ?? 0;
|
|
7659
|
+
f[base + 2] = sets[2] ?? 0;
|
|
7660
|
+
}
|
|
7472
7661
|
/* Which chain is bound, because the level and the weight both depend on it. */
|
|
7473
7662
|
f[at('uEnvironmentPrefiltered')] = this.quality.environmentPrefilter ? 1 : 0;
|
|
7474
7663
|
}
|
|
7664
|
+
/* DriftLight, or zeros that switch it off: see `driftLight/uniforms.ts`. */
|
|
7665
|
+
this.uploadLightField();
|
|
7666
|
+
const drift = this.driftUniforms;
|
|
7667
|
+
resolveDriftLight(
|
|
7668
|
+
this.driftAtlas === null ? null : this.lightField,
|
|
7669
|
+
this.probePassActive,
|
|
7670
|
+
camera.position,
|
|
7671
|
+
drift,
|
|
7672
|
+
);
|
|
7673
|
+
f.set(drift.light, at('uDriftLight'));
|
|
7674
|
+
f.set(drift.origin, at('uDriftLightOrigin'));
|
|
7475
7675
|
/*
|
|
7476
7676
|
* The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
|
|
7477
7677
|
*
|
|
@@ -7549,7 +7749,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7549
7749
|
* it every frame.
|
|
7550
7750
|
*/
|
|
7551
7751
|
if (this.probeEnabled) {
|
|
7552
|
-
const usable = this.
|
|
7752
|
+
const usable = this.probeReadable();
|
|
7553
7753
|
f[at('uEnvironmentEnabled')] = usable ? 1 : 0;
|
|
7554
7754
|
f[at('uEnvironmentMaxLod')] = this.probeMaxLod;
|
|
7555
7755
|
/*
|
|
@@ -7808,6 +8008,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7808
8008
|
|
|
7809
8009
|
/** Said once rather than every frame, for the reason `warnedFull` gives. */
|
|
7810
8010
|
private warnedMaterialsFull = false;
|
|
8011
|
+
private warnedSkyFull = false;
|
|
8012
|
+
private warnedParticlesTwice = false;
|
|
8013
|
+
/**
|
|
8014
|
+
* Which encoder draws are being recorded into, by count: a new one at every `beginFrame`, and one
|
|
8015
|
+
* of its own for each probe bake, restored after. What a batch drawn per encoder is checked by.
|
|
8016
|
+
*/
|
|
8017
|
+
private encoderEpoch = 0;
|
|
8018
|
+
private encoderEpochs = 0;
|
|
7811
8019
|
|
|
7812
8020
|
/**
|
|
7813
8021
|
* Draw one mesh.
|
|
@@ -8121,7 +8329,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8121
8329
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
8122
8330
|
/* The two order-independent buffers are two formats and two blend states, so a draw into
|
|
8123
8331
|
one must never share a cached pipeline with the same mesh drawn into the other. */
|
|
8124
|
-
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}
|
|
8332
|
+
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
|
|
8333
|
+
/* Culling is pipeline state here: a two-sided surface culls nothing. */
|
|
8334
|
+
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
8125
8335
|
|
|
8126
8336
|
/*
|
|
8127
8337
|
* The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
|
|
@@ -8166,6 +8376,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8166
8376
|
layer,
|
|
8167
8377
|
false,
|
|
8168
8378
|
this.oitMode,
|
|
8379
|
+
this.materialDoubleSided,
|
|
8169
8380
|
);
|
|
8170
8381
|
}
|
|
8171
8382
|
|
|
@@ -8399,7 +8610,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8399
8610
|
plain one would hand a draw the wrong layout. */
|
|
8400
8611
|
const key =
|
|
8401
8612
|
`${base}|inst${blend ? '|blend' : ''}` +
|
|
8402
|
-
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}
|
|
8613
|
+
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
8614
|
+
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
8403
8615
|
|
|
8404
8616
|
const pipelines = this.targetPipelines();
|
|
8405
8617
|
let pipeline = pipelines.peek(key);
|
|
@@ -8421,6 +8633,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8421
8633
|
depthWrite,
|
|
8422
8634
|
layer,
|
|
8423
8635
|
true,
|
|
8636
|
+
'none',
|
|
8637
|
+
this.materialDoubleSided,
|
|
8424
8638
|
);
|
|
8425
8639
|
}
|
|
8426
8640
|
|
|
@@ -8581,7 +8795,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8581
8795
|
f[atF('uFogNear')] = medium.fogNear;
|
|
8582
8796
|
f[atF('uFogFar')] = medium.fogFar;
|
|
8583
8797
|
|
|
8584
|
-
this.
|
|
8798
|
+
const fragmentSlot = this.scatterFrags.allocate();
|
|
8799
|
+
if (fragmentSlot === null) return;
|
|
8800
|
+
this.scatterFrags.writeBlock(fragmentSlot, this.scatterFragInts);
|
|
8585
8801
|
|
|
8586
8802
|
const scatterPipe = scatterPipeline(this.pipelines, this.surface.device, this.scatterLayout);
|
|
8587
8803
|
/* One dynamic offset, and a group of two uniform buffers: nothing sampled, nothing read. */
|
|
@@ -8590,7 +8806,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8590
8806
|
scatterCommand.pipeline = scatterPipe;
|
|
8591
8807
|
scatterCommand.bindGroup = this.scatterBindGroup;
|
|
8592
8808
|
scatterCommand.offsetA = slot;
|
|
8593
|
-
scatterCommand.
|
|
8809
|
+
scatterCommand.offsetB = fragmentSlot;
|
|
8810
|
+
scatterCommand.offsetCount = 2;
|
|
8594
8811
|
for (let index = 0; index < scatter.vertexBuffers.length; index++) {
|
|
8595
8812
|
scatterCommand.vertexBuffers[index] = scatter.vertexBuffers[index] as GPUBuffer;
|
|
8596
8813
|
}
|
|
@@ -8603,7 +8820,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8603
8820
|
const pass = this.openPass();
|
|
8604
8821
|
if (pass === null) return;
|
|
8605
8822
|
pass.setPipeline(scatterPipe);
|
|
8606
|
-
pass.setBindGroup(0, this.scatterBindGroup, [slot]);
|
|
8823
|
+
pass.setBindGroup(0, this.scatterBindGroup, [slot, fragmentSlot]);
|
|
8607
8824
|
for (let index = 0; index < scatter.vertexBuffers.length; index++) {
|
|
8608
8825
|
pass.setVertexBuffer(index, scatter.vertexBuffers[index] as GPUBuffer);
|
|
8609
8826
|
}
|
|
@@ -9264,6 +9481,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9264
9481
|
*/
|
|
9265
9482
|
private rebuildFlatGroupsForNewRings(): void {
|
|
9266
9483
|
this.flatBindGroups.clear();
|
|
9484
|
+
this.flatEnvironment = this.flatTextures('uEnvironment').view;
|
|
9267
9485
|
this.skinnedGroups.length = 0;
|
|
9268
9486
|
|
|
9269
9487
|
const albedo = this.albedo;
|
|
@@ -9290,6 +9508,27 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9290
9508
|
this.materials.dirty();
|
|
9291
9509
|
}
|
|
9292
9510
|
|
|
9511
|
+
/**
|
|
9512
|
+
* The environment view the cached flat groups bind: the grid, or the blank stand-in while the
|
|
9513
|
+
* grid may not be read. What `rebindEnvironment` compares against.
|
|
9514
|
+
*/
|
|
9515
|
+
private flatEnvironment: GPUTextureView | null = null;
|
|
9516
|
+
|
|
9517
|
+
/**
|
|
9518
|
+
* Rebuild the flat groups if, and only if, the environment they bind has changed.
|
|
9519
|
+
*
|
|
9520
|
+
* **Every cached group holds the environment view**, so a change of it makes all of them stale;
|
|
9521
|
+
* but nothing else a bake does touches a group. Rebuilding on every bake's two edges regardless
|
|
9522
|
+
* emptied the cache twice a bake, and every material built its group again inside the bake and
|
|
9523
|
+
* again after it: a courtyard re-baking a few faces a frame spent 28% of its CPU there on the
|
|
9524
|
+
* native host, where a `createBindGroup` costs most. What would make this wrong is a second
|
|
9525
|
+
* binding that a bake changes, which would have to be compared here too.
|
|
9526
|
+
*/
|
|
9527
|
+
private rebindEnvironment(): void {
|
|
9528
|
+
if (this.flatTextures('uEnvironment').view !== this.flatEnvironment)
|
|
9529
|
+
this.rebuildFlatBindGroup();
|
|
9530
|
+
}
|
|
9531
|
+
|
|
9293
9532
|
/**
|
|
9294
9533
|
* Rebuild the group for the open material, and drop every cached one.
|
|
9295
9534
|
*
|
|
@@ -9301,6 +9540,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9301
9540
|
*/
|
|
9302
9541
|
private rebuildFlatBindGroup(): void {
|
|
9303
9542
|
this.flatBindGroups.clear();
|
|
9543
|
+
this.flatEnvironment = this.flatTextures('uEnvironment').view;
|
|
9304
9544
|
if (
|
|
9305
9545
|
this.albedo === null &&
|
|
9306
9546
|
this.normalMap === null &&
|
|
@@ -9341,6 +9581,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9341
9581
|
*/
|
|
9342
9582
|
drawSky(camera: Camera, sky: SkyColors, env: Environment): void {
|
|
9343
9583
|
if (!this.canDraw()) return;
|
|
9584
|
+
this.frameSky = sky;
|
|
9344
9585
|
|
|
9345
9586
|
const f = this.skyFloats;
|
|
9346
9587
|
const at = (name: keyof typeof SKY_FIELDS): number => SKY_FIELDS[name].offset / 4;
|
|
@@ -9375,7 +9616,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9375
9616
|
f.set(medium.underwaterColor, at('uUnderwaterColor'));
|
|
9376
9617
|
f[at('uUnderwaterFactor')] = medium.underwaterFactor;
|
|
9377
9618
|
|
|
9378
|
-
this.
|
|
9619
|
+
const slot = this.skyUniforms.allocate();
|
|
9620
|
+
if (slot === null) {
|
|
9621
|
+
if (!this.warnedSkyFull) {
|
|
9622
|
+
this.warnedSkyFull = true;
|
|
9623
|
+
console.warn(
|
|
9624
|
+
`WebGPU: more than ${MAX_SKY_DRAWS} sky draws in a frame; the rest are skipped`,
|
|
9625
|
+
);
|
|
9626
|
+
}
|
|
9627
|
+
return;
|
|
9628
|
+
}
|
|
9629
|
+
this.skyUniforms.writeFloats(slot, 0, f);
|
|
9379
9630
|
|
|
9380
9631
|
const skyPipe = skyPipeline(this.pipelines, this.surface.device, this.skyLayout);
|
|
9381
9632
|
/* Reads nothing: the group is one uniform buffer, and the sky samples no attachment. */
|
|
@@ -9383,12 +9634,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9383
9634
|
if (skyCommand !== null) {
|
|
9384
9635
|
skyCommand.pipeline = skyPipe;
|
|
9385
9636
|
skyCommand.bindGroup = this.skyBindGroup;
|
|
9637
|
+
skyCommand.offsetA = slot;
|
|
9638
|
+
skyCommand.offsetCount = 1;
|
|
9386
9639
|
skyCommand.count = 3;
|
|
9387
9640
|
} else {
|
|
9388
9641
|
const pass = this.openPass();
|
|
9389
9642
|
if (pass === null) return;
|
|
9390
9643
|
pass.setPipeline(skyPipe);
|
|
9391
|
-
pass.setBindGroup(0, this.skyBindGroup);
|
|
9644
|
+
pass.setBindGroup(0, this.skyBindGroup, [slot]);
|
|
9392
9645
|
pass.draw(3);
|
|
9393
9646
|
}
|
|
9394
9647
|
}
|
|
@@ -9499,6 +9752,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9499
9752
|
if (aoStrength > 0 && this.frameProjection !== null) this.runOcclusion(encoder);
|
|
9500
9753
|
const bloomStrength = this.quality.bloom * this.bloomScale;
|
|
9501
9754
|
if (bloomStrength > 0) this.runBloom(encoder);
|
|
9755
|
+
/*
|
|
9756
|
+
* Eye adaptation, metered from the scene the composite shows, and local exposure's grid beside
|
|
9757
|
+
* it. The held brightness is kept for either, since local exposure moves regions relative to it.
|
|
9758
|
+
* See `setAutoExposure` and `setLocalExposure`.
|
|
9759
|
+
*/
|
|
9760
|
+
const localising = this.localExposure > 0;
|
|
9761
|
+
const measuring = (this.autoExposure > 0 || localising) && this.exposurePass !== null;
|
|
9762
|
+
const adapting = measuring && this.autoExposure > 0;
|
|
9763
|
+
const shownScene = this.reconstructing ? this.reconShownView : this.sceneColorView;
|
|
9764
|
+
if (measuring && shownScene !== null) {
|
|
9765
|
+
this.exposurePass?.run(encoder, shownScene, this.autoExposureDt, localising);
|
|
9766
|
+
}
|
|
9502
9767
|
|
|
9503
9768
|
/* The grade travels here only where the mesh pass gave it up. See `bindMeshPass`. */
|
|
9504
9769
|
const graded = this.quality.hdrScene;
|
|
@@ -9538,6 +9803,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9538
9803
|
/* The grade, sampled after the curve and before the veil. See `uGradeLut` in `rush.ts`. */
|
|
9539
9804
|
f[at('uGradeStrength')] = this.gradeStrength;
|
|
9540
9805
|
f[at('uGradeSize')] = this.gradeSize;
|
|
9806
|
+
/* The lens and the print. See `setVignette` and `setFilmGrain`. */
|
|
9807
|
+
f[at('uVignette')] = this.vignette;
|
|
9808
|
+
f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
|
|
9809
|
+
f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
|
|
9810
|
+
f[at('uGrain')] = this.grain;
|
|
9811
|
+
f[at('uGrain') + 1] = this.grainSeed;
|
|
9541
9812
|
device.queue.writeBuffer(this.rushUniforms, 0, this.rushStaging);
|
|
9542
9813
|
|
|
9543
9814
|
const pass = encoder.beginRenderPass({
|
|
@@ -9620,6 +9891,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9620
9891
|
a frame that never grades anything should not carry a texture, and a declared binding
|
|
9621
9892
|
must still have a complete one — the argument the bloom fallback above makes. */
|
|
9622
9893
|
...entry(RUSH_TEXTURES.uGradeLut, this.ensureGradeView()),
|
|
9894
|
+
/* The scene as the placeholder until adaptation is asked for: the strength is zero then,
|
|
9895
|
+
and a declared binding still needs a complete texture, as the bloom fallback says. */
|
|
9896
|
+
...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
|
|
9897
|
+
...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
|
|
9623
9898
|
],
|
|
9624
9899
|
layout: this.rushLayout,
|
|
9625
9900
|
});
|
|
@@ -10771,12 +11046,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10771
11046
|
otherwise halve the blur and leave half the pattern standing. */
|
|
10772
11047
|
const width = Math.max(1, this.aoTarget?.width ?? 1);
|
|
10773
11048
|
const height = Math.max(1, this.aoTarget?.height ?? 1);
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
11049
|
+
/* Across at the estimate's own scale, which the second axis needs whole; down decoded, so the
|
|
11050
|
+
composite reads open sky as one. The same two `ambientOcclusionPass.ts` writes. */
|
|
11051
|
+
for (const [index, step, scale] of [
|
|
11052
|
+
[0, [1 / width, 0], 1],
|
|
11053
|
+
[1, [0, 1 / height], 1 / AO_STORE],
|
|
10777
11054
|
] as const) {
|
|
10778
11055
|
f[slot(index) + at('uStep')] = step[0];
|
|
10779
11056
|
f[slot(index) + at('uStep') + 1] = step[1];
|
|
11057
|
+
f[slot(index) + at('uAoScale')] = scale;
|
|
10780
11058
|
f[slot(index) + at('uDepthToViewZ')] = inv[10] ?? 0;
|
|
10781
11059
|
f[slot(index) + at('uDepthToViewZ') + 1] = inv[14] ?? 0;
|
|
10782
11060
|
f[slot(index) + at('uDepthToViewZ') + 2] = inv[11] ?? 0;
|
|
@@ -11509,8 +11787,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11509
11787
|
const swap = this.swapView();
|
|
11510
11788
|
if (swap === null) return null;
|
|
11511
11789
|
|
|
11512
|
-
|
|
11513
|
-
|
|
11790
|
+
/*
|
|
11791
|
+
* **The drawing buffer's size, not the scene depth's.** The overlay draws on the swap image,
|
|
11792
|
+
* past the resolve, and a reconstruction draws the scene depth small: sized from that, the
|
|
11793
|
+
* device refused the pass beside a full-size swap image and every overlay went missing.
|
|
11794
|
+
*/
|
|
11795
|
+
const width = Math.max(1, this.surface.canvas.width);
|
|
11796
|
+
const height = Math.max(1, this.surface.canvas.height);
|
|
11514
11797
|
if (
|
|
11515
11798
|
this.overlayDepth === null ||
|
|
11516
11799
|
this.overlayDepth.width !== width ||
|
|
@@ -11868,9 +12151,31 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11868
12151
|
* had stopped declaring. What the guard is actually for is not *building* a composer for a
|
|
11869
12152
|
* profile that never declares anything, so that is all it now guards.
|
|
11870
12153
|
*/
|
|
12154
|
+
if (this.fieldComposer === null && this.distanceFields.length === 0) return;
|
|
12155
|
+
/*
|
|
12156
|
+
* **As many cascades as reach the probe grid**, and rebuilt when the grid asks for a different
|
|
12157
|
+
* number: the far end of a courtyard longer than the default 16 m was traced against no field
|
|
12158
|
+
* at all and lit as open sky from every side. See `gi/fieldReach.ts`.
|
|
12159
|
+
*/
|
|
12160
|
+
const grid = this.indirectGrid();
|
|
12161
|
+
const cascades =
|
|
12162
|
+
grid === null
|
|
12163
|
+
? DEFAULT_FIELD_COMPOSE.cascades
|
|
12164
|
+
: cascadesForGrid(
|
|
12165
|
+
grid,
|
|
12166
|
+
DEFAULT_FIELD_COMPOSE.radius,
|
|
12167
|
+
DEFAULT_FIELD_COMPOSE.cascades,
|
|
12168
|
+
MAX_FIELD_CASCADES,
|
|
12169
|
+
);
|
|
12170
|
+
if (this.fieldComposer !== null && this.fieldComposer.cascadeCount !== cascades) {
|
|
12171
|
+
this.fieldComposer.dispose();
|
|
12172
|
+
this.fieldComposer = null;
|
|
12173
|
+
}
|
|
11871
12174
|
if (this.fieldComposer === null) {
|
|
11872
|
-
|
|
11873
|
-
|
|
12175
|
+
this.fieldComposer = new FieldComposer(this.surface.device, {
|
|
12176
|
+
...DEFAULT_FIELD_COMPOSE,
|
|
12177
|
+
cascades,
|
|
12178
|
+
});
|
|
11874
12179
|
}
|
|
11875
12180
|
this.fieldComposer.compose(encoder, this.distanceFields, this.frameEye);
|
|
11876
12181
|
}
|
|
@@ -11938,12 +12243,34 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11938
12243
|
* never said what was lighting it.
|
|
11939
12244
|
*/
|
|
11940
12245
|
const env = this.frameEnv;
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
12246
|
+
/*
|
|
12247
|
+
* And the scene's summed fixed lights, which the trace adds to what a hit receives: the bounce
|
|
12248
|
+
* of every candle, not only of the ones near enough to be shaded exactly.
|
|
12249
|
+
*/
|
|
12250
|
+
resolveDriftLight(
|
|
12251
|
+
this.bakeLightField === null ? null : this.lightField,
|
|
12252
|
+
true,
|
|
12253
|
+
this.frameEye,
|
|
12254
|
+
this.bakeDriftUniforms,
|
|
12255
|
+
);
|
|
12256
|
+
/* And the frame's exact lights, flickering and moving as the frame shades them. */
|
|
12257
|
+
this.bakeLamps.count = resolveBounceLights(env, this.bounceLights);
|
|
12258
|
+
this.bakeLamps.falloff = this.lights.falloff;
|
|
12259
|
+
this.probeBaker.bake(
|
|
12260
|
+
encoder,
|
|
12261
|
+
field,
|
|
12262
|
+
grid,
|
|
12263
|
+
this.probeBaked,
|
|
12264
|
+
this.bakeLightField,
|
|
12265
|
+
this.bakeLamps,
|
|
12266
|
+
{
|
|
12267
|
+
direction: env?.directionalDir ?? NO_SUN,
|
|
12268
|
+
colour: env?.directionalColor ?? NO_SUN,
|
|
12269
|
+
/* What a ray that left the world finds: the sky this frame drew, or its ambient. */
|
|
12270
|
+
ambient: env?.ambient ?? NO_SUN,
|
|
12271
|
+
sky: this.frameSky,
|
|
12272
|
+
},
|
|
12273
|
+
);
|
|
11947
12274
|
/*
|
|
11948
12275
|
* **The bake is what fills the layers now, so it is what marks them filled.** `probeBaked`
|
|
11949
12276
|
* gates the whole array, and a grid nothing rasterised would otherwise never be sampled — the
|
|
@@ -12170,7 +12497,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12170
12497
|
* currently open. Offering it earlier hands a pass a texture of undefined contents, which is
|
|
12171
12498
|
* the failure `probeBaked` was introduced for on the forward path.
|
|
12172
12499
|
*/
|
|
12173
|
-
const usable = this.probeView !== null && this.
|
|
12500
|
+
const usable = this.probeView !== null && this.probeReadable();
|
|
12174
12501
|
if (usable) {
|
|
12175
12502
|
const environment = this.passEnvironment;
|
|
12176
12503
|
environment.view = this.probeView;
|
|
@@ -12658,6 +12985,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12658
12985
|
*/
|
|
12659
12986
|
|
|
12660
12987
|
private bloomScale = 1;
|
|
12988
|
+
/** In scene units, once `setBloom` has moved it; null reads the profile's. */
|
|
12989
|
+
private bloomThresholdSet: number | null = null;
|
|
12661
12990
|
private rushStrength = 0;
|
|
12662
12991
|
private motionBlurScale = 1;
|
|
12663
12992
|
/**
|
|
@@ -12669,6 +12998,51 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12669
12998
|
*/
|
|
12670
12999
|
private readonly veilColor: Vec3 = [0, 0, 0];
|
|
12671
13000
|
private veilAlpha = 0;
|
|
13001
|
+
private vignette = 0;
|
|
13002
|
+
private grain = 0;
|
|
13003
|
+
private grainSeed = 0;
|
|
13004
|
+
private warnedLookWithoutComposite = false;
|
|
13005
|
+
/** Eye adaptation: how far, this frame's time step, and the pass once asked for. */
|
|
13006
|
+
private autoExposure = 0;
|
|
13007
|
+
private autoExposureDt = 0;
|
|
13008
|
+
/** How far each region is brought toward the frame. See `setLocalExposure`. */
|
|
13009
|
+
private localExposure = 0;
|
|
13010
|
+
private exposurePass: ExposurePass | null = null;
|
|
13011
|
+
|
|
13012
|
+
/**
|
|
13013
|
+
* How strongly the lens darkens the frame's corners, held until changed: 0 is none, 0.5 is about
|
|
13014
|
+
* 1.2 stops at the corner. Applied to scene light before the tone curve, as a lens loses light;
|
|
13015
|
+
* see `filmLook.ts`. Needs `screenEffects`, and says so once rather than doing nothing.
|
|
13016
|
+
*/
|
|
13017
|
+
setVignette(strength: number): void {
|
|
13018
|
+
if (!this.hasComposite) {
|
|
13019
|
+
this.warnLookWithoutComposite(strength);
|
|
13020
|
+
return;
|
|
13021
|
+
}
|
|
13022
|
+
this.vignette = clampVignette(strength);
|
|
13023
|
+
}
|
|
13024
|
+
|
|
13025
|
+
/**
|
|
13026
|
+
* Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
|
|
13027
|
+
* grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
|
|
13028
|
+
* its caller: pass a new one each frame for grain that moves, and the same one for a still that
|
|
13029
|
+
* is identical run to run. Applied last, after the grade and the veil; see `filmLook.ts`.
|
|
13030
|
+
*/
|
|
13031
|
+
setFilmGrain(strength: number, seed: number): void {
|
|
13032
|
+
if (!this.hasComposite) {
|
|
13033
|
+
this.warnLookWithoutComposite(strength);
|
|
13034
|
+
return;
|
|
13035
|
+
}
|
|
13036
|
+
this.grain = clampGrain(strength);
|
|
13037
|
+
this.grainSeed = grainSeed(seed);
|
|
13038
|
+
}
|
|
13039
|
+
|
|
13040
|
+
private warnLookWithoutComposite(strength: number): void {
|
|
13041
|
+
if (strength > 0 && !this.warnedLookWithoutComposite) {
|
|
13042
|
+
this.warnedLookWithoutComposite = true;
|
|
13043
|
+
console.warn(FILM_LOOK_WITHOUT_COMPOSITE);
|
|
13044
|
+
}
|
|
13045
|
+
}
|
|
12672
13046
|
|
|
12673
13047
|
/*
|
|
12674
13048
|
* **These three warned that the composite did not exist, and went on warning after it did.**
|
|
@@ -12754,8 +13128,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12754
13128
|
this.gradeSize = lut.size;
|
|
12755
13129
|
}
|
|
12756
13130
|
|
|
12757
|
-
setBloom(scale: number): void {
|
|
13131
|
+
setBloom(scale: number, threshold?: number): void {
|
|
12758
13132
|
this.bloomScale = Math.min(Math.max(scale, 0), 1);
|
|
13133
|
+
if (threshold === undefined) return;
|
|
13134
|
+
const next = bloomThresholdOf(threshold);
|
|
13135
|
+
if (next === this.bloomThresholdSet) return;
|
|
13136
|
+
this.bloomThresholdSet = next;
|
|
13137
|
+
/* The blocks are written once per size, so a moved threshold rewrites them now. */
|
|
13138
|
+
if (this.bloomLevels.length > 0) this.writeBloomBlocks();
|
|
12759
13139
|
}
|
|
12760
13140
|
|
|
12761
13141
|
/**
|
|
@@ -12788,6 +13168,62 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12788
13168
|
setCameraMotionBlur(scale: number): void {
|
|
12789
13169
|
this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
|
|
12790
13170
|
}
|
|
13171
|
+
/**
|
|
13172
|
+
* The camera has cut: this frame is a new shot, not the next moment of the last one.
|
|
13173
|
+
*
|
|
13174
|
+
* **Everything temporal reprojects through the previous frame's view**: the motion blur measures
|
|
13175
|
+
* its smear against it, and the temporal resolve reads its history through it. A cut is a jump
|
|
13176
|
+
* the renderer cannot tell from a fast camera, so without this a transport that seeks, a respawn
|
|
13177
|
+
* or an edit smears its first frame along the whole jump and blends in a picture of somewhere
|
|
13178
|
+
* else. The caller knows it cut; this is how it says so, before the frame's `beginFrame`.
|
|
13179
|
+
*
|
|
13180
|
+
* What it gives up is one frame of history: the frame after a cut has no blur and no temporal
|
|
13181
|
+
* blend, as the first frame of a session has none. Nothing else is reset.
|
|
13182
|
+
*/
|
|
13183
|
+
cameraCut(): void {
|
|
13184
|
+
this.hasPreviousView = false;
|
|
13185
|
+
this.temporalHistory.invalidate();
|
|
13186
|
+
/* The reconstruction keeps histories of its own, and they are the last shot's too. */
|
|
13187
|
+
this.reconHasHistory = false;
|
|
13188
|
+
/* And the eye was adapted to the last shot. */
|
|
13189
|
+
this.exposurePass?.cut();
|
|
13190
|
+
}
|
|
13191
|
+
|
|
13192
|
+
/**
|
|
13193
|
+
* Eye adaptation. **The other backend's `setAutoExposure` carries the reasoning**, whole; this is
|
|
13194
|
+
* the same setter over the same state, and the pass is built the first time it is asked for, so
|
|
13195
|
+
* a renderer that never adapts carries none of it.
|
|
13196
|
+
*/
|
|
13197
|
+
setAutoExposure(strength: number, dtSec: number): void {
|
|
13198
|
+
if (!this.hasComposite || !this.quality.hdrScene) {
|
|
13199
|
+
this.warnLookWithoutComposite(strength);
|
|
13200
|
+
return;
|
|
13201
|
+
}
|
|
13202
|
+
this.autoExposure = clampAutoExposure(strength);
|
|
13203
|
+
this.autoExposureDt = dtSec;
|
|
13204
|
+
if (this.autoExposure > 0) this.ensureExposurePass();
|
|
13205
|
+
}
|
|
13206
|
+
|
|
13207
|
+
/**
|
|
13208
|
+
* Local exposure. **The other backend's `setLocalExposure` carries the reasoning**, whole; this is
|
|
13209
|
+
* the same setter over the same state, sharing eye adaptation's pass, which is built the first
|
|
13210
|
+
* time either is asked for.
|
|
13211
|
+
*/
|
|
13212
|
+
setLocalExposure(strength: number): void {
|
|
13213
|
+
if (!this.hasComposite || !this.quality.hdrScene) {
|
|
13214
|
+
this.warnLookWithoutComposite(strength);
|
|
13215
|
+
return;
|
|
13216
|
+
}
|
|
13217
|
+
this.localExposure = clampLocalExposure(strength);
|
|
13218
|
+
if (this.localExposure > 0) this.ensureExposurePass();
|
|
13219
|
+
}
|
|
13220
|
+
|
|
13221
|
+
/** The exposure pass, built once; the composite's group holds placeholders until it exists. */
|
|
13222
|
+
private ensureExposurePass(): void {
|
|
13223
|
+
if (this.exposurePass !== null) return;
|
|
13224
|
+
this.exposurePass = new ExposurePass(this.surface.device, this.pipelines, this.postSampler);
|
|
13225
|
+
this.rebuildRushBindGroup();
|
|
13226
|
+
}
|
|
12791
13227
|
|
|
12792
13228
|
/**
|
|
12793
13229
|
* Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
|
|
@@ -13027,6 +13463,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13027
13463
|
* over a lobe-convolved one.
|
|
13028
13464
|
*/
|
|
13029
13465
|
private readonly probe: GPUTexture | null;
|
|
13466
|
+
/** The views, groups and attachments a bake draws with, made once. See `probeTargets.ts`. */
|
|
13467
|
+
private readonly probeTargets: ProbeTargets | null;
|
|
13030
13468
|
/**
|
|
13031
13469
|
* The prefiltered cube: what the lit pass samples, one GGX-convolved level per roughness.
|
|
13032
13470
|
*
|
|
@@ -13058,6 +13496,19 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13058
13496
|
private readonly probeMipLayout: GPUBindGroupLayout;
|
|
13059
13497
|
/** The photometric atlas, allocated on first use. See `setIesProfiles`. */
|
|
13060
13498
|
private iesTexture: GPUTexture | null = null;
|
|
13499
|
+
/** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
|
|
13500
|
+
private lightField: LightField | null = null;
|
|
13501
|
+
private driftIndex: GPUTexture | null = null;
|
|
13502
|
+
private driftAtlas: GPUTexture | null = null;
|
|
13503
|
+
private driftIndexView: GPUTextureView | null = null;
|
|
13504
|
+
private driftAtlasView: GPUTextureView | null = null;
|
|
13505
|
+
private readonly driftUniforms = createDriftLightUniforms();
|
|
13506
|
+
/** The same field resolved for a traced probe, and what the baker binds; null while not whole. */
|
|
13507
|
+
private readonly bakeDriftUniforms = createDriftLightUniforms();
|
|
13508
|
+
private bakeLightField: ProbeBakeLightField | null = null;
|
|
13509
|
+
/** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
|
|
13510
|
+
private readonly bounceLights = createBounceLights();
|
|
13511
|
+
private readonly bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
|
|
13061
13512
|
private iesView: GPUTextureView | null = null;
|
|
13062
13513
|
private cookieTexture: GPUTexture | null = null;
|
|
13063
13514
|
private cookieView: GPUTextureView | null = null;
|
|
@@ -13082,8 +13533,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13082
13533
|
* mirroring uninitialised memory is worse than a car mirroring a gradient — the rule one probe
|
|
13083
13534
|
* already had, unchanged by there being more of them.
|
|
13084
13535
|
*/
|
|
13085
|
-
private
|
|
13086
|
-
|
|
13536
|
+
private probeSweeps: ProbeSweeps | null = null;
|
|
13537
|
+
/** `uProbeGridSets`, rewritten in place. */
|
|
13538
|
+
private readonly probeSets = new Float32Array(3);
|
|
13087
13539
|
/**
|
|
13088
13540
|
* Whether a bake is running, which is when the cube may be neither sampled nor bound.
|
|
13089
13541
|
*
|
|
@@ -13091,15 +13543,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13091
13543
|
* load-bearing rather than defensive: WebGPU rejects a pass that holds the texture it writes.
|
|
13092
13544
|
*/
|
|
13093
13545
|
private probePassActive = false;
|
|
13546
|
+
/** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
|
|
13547
|
+
private probeBounce = false;
|
|
13548
|
+
|
|
13549
|
+
/**
|
|
13550
|
+
* Whether the shading may read the grid now: once every probe is filled, and not during a bake
|
|
13551
|
+
* unless it is a bounce bake. A bounce bake's faces are drawn into the probe's own cube and only
|
|
13552
|
+
* the resolve writes the array, in a pass of its own, so no pass both samples and writes it.
|
|
13553
|
+
*/
|
|
13554
|
+
private probeReadable(): boolean {
|
|
13555
|
+
return this.probeBaked && (!this.probePassActive || this.probeBounce);
|
|
13556
|
+
}
|
|
13094
13557
|
|
|
13095
13558
|
/** How many mip levels the roughness blur has to work with. `flat.ts` reads this. */
|
|
13096
13559
|
private get probeMaxLod(): number {
|
|
13097
13560
|
return ggxMaxLevelFor(this.probeEdge);
|
|
13098
13561
|
}
|
|
13099
13562
|
|
|
13100
|
-
/** Whether
|
|
13563
|
+
/** Whether a whole sweep of the grid has been convolved. See `probeSweeps.ts`. */
|
|
13101
13564
|
private get probeBaked(): boolean {
|
|
13102
|
-
return this.probes !== null && this.
|
|
13565
|
+
return this.probes !== null && this.probeSweeps?.ready === true;
|
|
13103
13566
|
}
|
|
13104
13567
|
|
|
13105
13568
|
/**
|
|
@@ -13119,11 +13582,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13119
13582
|
* mistake, and `setProbeGrid` says it about the array being replaced.
|
|
13120
13583
|
*/
|
|
13121
13584
|
private markProbeFilled(layer: number): void {
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
this.
|
|
13125
|
-
this.probeFilledCount++;
|
|
13126
|
-
if (this.probeBaked) this.rebuildFlatGroupsForNewRings();
|
|
13585
|
+
const was = this.probeBaked;
|
|
13586
|
+
this.probeSweeps?.baked(layer);
|
|
13587
|
+
if (!was && this.probeBaked) this.rebuildFlatGroupsForNewRings();
|
|
13127
13588
|
}
|
|
13128
13589
|
|
|
13129
13590
|
/**
|
|
@@ -13134,6 +13595,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13134
13595
|
*/
|
|
13135
13596
|
private allocateProbeArray(layers: number, device: GPUDevice): void {
|
|
13136
13597
|
this.probeArray?.destroy();
|
|
13598
|
+
this.probeTargets?.forgetArray();
|
|
13137
13599
|
this.probeArray = device.createTexture({
|
|
13138
13600
|
label: 'probe.array',
|
|
13139
13601
|
size: [this.probeEdge, this.probeEdge, layers],
|
|
@@ -13143,8 +13605,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13143
13605
|
usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
|
|
13144
13606
|
});
|
|
13145
13607
|
this.probeView = this.probeArray.createView({ dimension: '2d-array' });
|
|
13146
|
-
this.probeFilled = new Uint8Array(layers);
|
|
13147
|
-
this.probeFilledCount = 0;
|
|
13148
13608
|
|
|
13149
13609
|
/*
|
|
13150
13610
|
* **Cleared here, because a texture no pass has written holds undefined contents** — the same
|
|
@@ -13201,8 +13661,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13201
13661
|
const current = this.probes;
|
|
13202
13662
|
if (current !== null && this.probeArray !== null && sameGrid(current, grid)) return true;
|
|
13203
13663
|
this.probes = grid;
|
|
13204
|
-
|
|
13205
|
-
|
|
13664
|
+
/* A traced grid writes its layers itself, so it keeps one set. See `ProbeGridOptions.crossfade`. */
|
|
13665
|
+
const crossfade = grid.crossfade && !this.quality.indirectLight;
|
|
13666
|
+
const sets = crossfade ? 3 : 1;
|
|
13667
|
+
if (
|
|
13668
|
+
this.probeArray === null ||
|
|
13669
|
+
this.probeArray.depthOrArrayLayers !== grid.layers * sets ||
|
|
13670
|
+
this.probeSweeps?.sets !== sets
|
|
13671
|
+
) {
|
|
13672
|
+
this.probeSweeps = new ProbeSweeps(grid.layers, crossfade);
|
|
13673
|
+
this.allocateProbeArray(grid.layers * sets, this.surface.device);
|
|
13206
13674
|
this.rebuildFlatGroupsForNewRings();
|
|
13207
13675
|
}
|
|
13208
13676
|
return true;
|
|
@@ -13278,40 +13746,20 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13278
13746
|
camera.roll = 0;
|
|
13279
13747
|
|
|
13280
13748
|
const { device } = this.surface;
|
|
13281
|
-
const
|
|
13282
|
-
|
|
13283
|
-
size: [this.probeSize, this.probeSize],
|
|
13284
|
-
format: DEPTH_FORMAT,
|
|
13285
|
-
/*
|
|
13286
|
-
* **The world's sample count, because a bake draws the world with the world's pipelines.**
|
|
13287
|
-
* A pipeline whose count disagrees with its attachment is rejected at `finish` and takes
|
|
13288
|
-
* the whole bake with it, and WebGPU requires every attachment in a pass to agree — so a
|
|
13289
|
-
* single-sample depth beside a four-sample colour is refused in words that read as a depth
|
|
13290
|
-
* problem and are a multisampling one.
|
|
13291
|
-
*/
|
|
13292
|
-
sampleCount: this.samples,
|
|
13293
|
-
usage: 0x10, // RENDER_ATTACHMENT
|
|
13294
|
-
});
|
|
13295
|
-
const depthView = depth.createView();
|
|
13749
|
+
const targets = this.probeTargets;
|
|
13750
|
+
if (targets === null) return false;
|
|
13296
13751
|
/*
|
|
13297
|
-
* The
|
|
13298
|
-
*
|
|
13299
|
-
*
|
|
13300
|
-
*
|
|
13301
|
-
*
|
|
13302
|
-
*
|
|
13752
|
+
* The depth, at the world's sample count because a bake draws the world with the world's
|
|
13753
|
+
* pipelines: a pipeline whose count disagrees with its attachment is rejected at `finish` and
|
|
13754
|
+
* takes the whole bake with it, in words that read as a depth problem and are a multisampling
|
|
13755
|
+
* one. And the multisampled twin the faces are drawn into and resolved from, since **a cube
|
|
13756
|
+
* face cannot be both multisampled and sampled** — the same bind the mirror is in. Both kept
|
|
13757
|
+
* between bakes: see `probeTargets.ts`.
|
|
13303
13758
|
*/
|
|
13304
|
-
const colorMsaa =
|
|
13305
|
-
this.samples
|
|
13306
|
-
|
|
13307
|
-
|
|
13308
|
-
size: [this.probeSize, this.probeSize],
|
|
13309
|
-
format: this.pipelines.format,
|
|
13310
|
-
sampleCount: this.samples,
|
|
13311
|
-
usage: 0x10, // RENDER_ATTACHMENT
|
|
13312
|
-
})
|
|
13313
|
-
: null;
|
|
13314
|
-
const colorMsaaView = colorMsaa?.createView() ?? null;
|
|
13759
|
+
const { depth: depthView, colorMsaa: colorMsaaView } = targets.attachments(
|
|
13760
|
+
this.samples,
|
|
13761
|
+
this.pipelines.format,
|
|
13762
|
+
);
|
|
13315
13763
|
|
|
13316
13764
|
/*
|
|
13317
13765
|
* The frame's own pass is set aside and restored, because `drawFace` calls straight back into
|
|
@@ -13330,16 +13778,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13330
13778
|
*/
|
|
13331
13779
|
if (this.quality.frameGraph) this.flushGraph();
|
|
13332
13780
|
const savedEncoder = this.encoder;
|
|
13781
|
+
/*
|
|
13782
|
+
* **The rings are marked here and given back after the submit below**, because this bake is its
|
|
13783
|
+
* own encoder: once it is submitted nothing in the frame can read its slots, and a grid baked in
|
|
13784
|
+
* one call would otherwise spend the frame's whole ring on probes and skip the draws past it.
|
|
13785
|
+
*/
|
|
13786
|
+
this.markRings();
|
|
13787
|
+
const skinMark = this.skinPalettes.mark();
|
|
13788
|
+
const frameEpoch = this.encoderEpoch;
|
|
13789
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
13333
13790
|
const encoder = device.createCommandEncoder({ label: 'probe' });
|
|
13334
13791
|
this.encoder = encoder;
|
|
13335
13792
|
/*
|
|
13336
|
-
* Fenced before the first face and released after the last,
|
|
13337
|
-
*
|
|
13338
|
-
*
|
|
13339
|
-
*
|
|
13793
|
+
* Fenced before the first face and released after the last, so the grid is not bound while a
|
|
13794
|
+
* plain bake is drawn. **Rebuilt only where that changes what the group binds**: a bounce bake
|
|
13795
|
+
* of a whole grid binds it exactly as the frame does, and it is the bake a scene repeats every
|
|
13796
|
+
* frame, a few faces at a time. See `rebindEnvironment`.
|
|
13340
13797
|
*/
|
|
13798
|
+
this.probeBounce = options?.bounce ?? false;
|
|
13341
13799
|
this.probePassActive = true;
|
|
13342
|
-
this.
|
|
13800
|
+
this.rebindEnvironment();
|
|
13343
13801
|
/*
|
|
13344
13802
|
* **A probe stores radiance, not display pixels.** See `renderer.ts` for the whole argument:
|
|
13345
13803
|
* baking through the output transform crushes every highlight with the tone curve before it
|
|
@@ -13352,7 +13810,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13352
13810
|
this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
|
|
13353
13811
|
this.materials.dirty();
|
|
13354
13812
|
|
|
13355
|
-
|
|
13813
|
+
/* Only the faces asked for; the chain waits for the sixth. See `ProbeBakeOptions.faces`. */
|
|
13814
|
+
const [firstFace, endFace] = probeFaceRange(options);
|
|
13815
|
+
for (let face = firstFace; face < endFace; face++) {
|
|
13356
13816
|
const aim = PROBE_FACES[face];
|
|
13357
13817
|
if (aim === undefined) continue;
|
|
13358
13818
|
camera.yaw = aim.yaw;
|
|
@@ -13363,13 +13823,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13363
13823
|
*/
|
|
13364
13824
|
camera.updateMatrices(1);
|
|
13365
13825
|
|
|
13366
|
-
const faceView =
|
|
13367
|
-
dimension: '2d',
|
|
13368
|
-
baseArrayLayer: face,
|
|
13369
|
-
arrayLayerCount: 1,
|
|
13370
|
-
baseMipLevel: 0,
|
|
13371
|
-
mipLevelCount: 1,
|
|
13372
|
-
});
|
|
13826
|
+
const faceView = targets.faceViews[face] as GPUTextureView;
|
|
13373
13827
|
this.pass = encoder.beginRenderPass({
|
|
13374
13828
|
label: `probe.face${face}`,
|
|
13375
13829
|
colorAttachments: [
|
|
@@ -13418,20 +13872,22 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13418
13872
|
this.pass.end();
|
|
13419
13873
|
}
|
|
13420
13874
|
this.pass = null;
|
|
13421
|
-
|
|
13875
|
+
const whole = endFace === PROBE_FACES.length;
|
|
13876
|
+
if (whole) this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
|
|
13422
13877
|
this.flushRings();
|
|
13423
13878
|
device.queue.submit([encoder.finish()]);
|
|
13424
|
-
|
|
13425
|
-
|
|
13879
|
+
this.rewindRings(skinMark);
|
|
13880
|
+
this.encoderEpoch = frameEpoch;
|
|
13426
13881
|
|
|
13427
13882
|
this.encoder = savedEncoder;
|
|
13428
13883
|
this.pass = savedPass;
|
|
13429
|
-
this.markProbeFilled(layer);
|
|
13884
|
+
if (whole) this.markProbeFilled(layer);
|
|
13430
13885
|
this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
|
|
13431
13886
|
this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
|
|
13432
13887
|
this.materials.dirty();
|
|
13433
13888
|
this.probePassActive = false;
|
|
13434
|
-
this.
|
|
13889
|
+
this.probeBounce = false;
|
|
13890
|
+
this.rebindEnvironment();
|
|
13435
13891
|
return true;
|
|
13436
13892
|
}
|
|
13437
13893
|
|
|
@@ -13582,6 +14038,92 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13582
14038
|
this.rebuildFlatBindGroup();
|
|
13583
14039
|
}
|
|
13584
14040
|
|
|
14041
|
+
/**
|
|
14042
|
+
* Sum a scene's many fixed lights into a DriftLight field. The same contract as
|
|
14043
|
+
* `Renderer.createLightField`; see `driftLight/lightField.ts`.
|
|
14044
|
+
*/
|
|
14045
|
+
createLightField(
|
|
14046
|
+
lights: readonly LightFieldSource[],
|
|
14047
|
+
options: Omit<LightFieldOptions, 'falloff'> = {},
|
|
14048
|
+
): LightField {
|
|
14049
|
+
this.forgetLightFieldVolumes();
|
|
14050
|
+
const field = new LightField(lights, { ...options, falloff: this.quality.pointLightFalloff });
|
|
14051
|
+
this.lightField = field;
|
|
14052
|
+
return field;
|
|
14053
|
+
}
|
|
14054
|
+
|
|
14055
|
+
/** Let go of the field and its volumes; the scene's lights are all exact again. */
|
|
14056
|
+
disposeLightField(): void {
|
|
14057
|
+
this.forgetLightFieldVolumes();
|
|
14058
|
+
this.lightField = null;
|
|
14059
|
+
}
|
|
14060
|
+
|
|
14061
|
+
private forgetLightFieldVolumes(): void {
|
|
14062
|
+
const bound = this.driftAtlas !== null;
|
|
14063
|
+
this.driftIndex?.destroy();
|
|
14064
|
+
this.driftAtlas?.destroy();
|
|
14065
|
+
this.driftIndex = null;
|
|
14066
|
+
this.driftAtlas = null;
|
|
14067
|
+
this.driftIndexView = null;
|
|
14068
|
+
this.driftAtlasView = null;
|
|
14069
|
+
this.bakeLightField = null;
|
|
14070
|
+
/* The groups hold the destroyed volumes, and a group holding a destroyed texture fails. */
|
|
14071
|
+
if (bound && !this.surface.lost) this.rebuildFlatBindGroup();
|
|
14072
|
+
}
|
|
14073
|
+
|
|
14074
|
+
/**
|
|
14075
|
+
* Upload the field's two volumes, once, when its last brick has baked, and rebuild the groups
|
|
14076
|
+
* that bind them: nothing samples them before that, so uploading as bricks land would be work
|
|
14077
|
+
* for no picture, and a rebuild per brick would be a bind group per brick.
|
|
14078
|
+
*/
|
|
14079
|
+
private uploadLightField(): void {
|
|
14080
|
+
const field = this.lightField;
|
|
14081
|
+
if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
|
|
14082
|
+
return;
|
|
14083
|
+
}
|
|
14084
|
+
if (this.surface.lost) return;
|
|
14085
|
+
const { device } = this.surface;
|
|
14086
|
+
const [ix, iy, iz] = field.layout.dims;
|
|
14087
|
+
const index = device.createTexture({
|
|
14088
|
+
label: 'driftLight.index',
|
|
14089
|
+
size: [ix, iy, iz],
|
|
14090
|
+
dimension: '3d',
|
|
14091
|
+
format: 'r32uint',
|
|
14092
|
+
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
14093
|
+
});
|
|
14094
|
+
device.queue.writeTexture(
|
|
14095
|
+
{ texture: index },
|
|
14096
|
+
field.layout.index,
|
|
14097
|
+
{ bytesPerRow: ix * 4, rowsPerImage: iy },
|
|
14098
|
+
[ix, iy, iz],
|
|
14099
|
+
);
|
|
14100
|
+
const [aw, ah, ad] = field.atlasSize;
|
|
14101
|
+
const atlas = device.createTexture({
|
|
14102
|
+
label: 'driftLight.atlas',
|
|
14103
|
+
size: [aw, ah, ad],
|
|
14104
|
+
dimension: '3d',
|
|
14105
|
+
format: 'rgba16float',
|
|
14106
|
+
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
14107
|
+
});
|
|
14108
|
+
device.queue.writeTexture(
|
|
14109
|
+
{ texture: atlas },
|
|
14110
|
+
field.atlas,
|
|
14111
|
+
{ bytesPerRow: aw * 8, rowsPerImage: ah },
|
|
14112
|
+
[aw, ah, ad],
|
|
14113
|
+
);
|
|
14114
|
+
this.driftIndex = index;
|
|
14115
|
+
this.driftAtlas = atlas;
|
|
14116
|
+
this.driftIndexView = index.createView({ dimension: '3d' });
|
|
14117
|
+
this.driftAtlasView = atlas.createView({ dimension: '3d' });
|
|
14118
|
+
this.bakeLightField = {
|
|
14119
|
+
index: this.driftIndexView,
|
|
14120
|
+
atlas: this.driftAtlasView,
|
|
14121
|
+
light: this.bakeDriftUniforms.light,
|
|
14122
|
+
origin: this.bakeDriftUniforms.origin,
|
|
14123
|
+
};
|
|
14124
|
+
this.rebuildFlatBindGroup();
|
|
14125
|
+
}
|
|
14126
|
+
|
|
13585
14127
|
setIesProfiles(profiles: readonly PhotometricProfile[]): void {
|
|
13586
14128
|
if (this.surface.lost) return;
|
|
13587
14129
|
const { device } = this.surface;
|
|
@@ -13629,6 +14171,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13629
14171
|
/** Each level from the one above it, six faces at a time. See `probePass.ts`. */
|
|
13630
14172
|
private buildProbeChain(encoder: GPUCommandEncoder, probe: GPUTexture, layer: number): void {
|
|
13631
14173
|
const { device } = this.surface;
|
|
14174
|
+
const targets = this.probeTargets;
|
|
14175
|
+
if (targets === null) return;
|
|
13632
14176
|
/*
|
|
13633
14177
|
* **Reset and flushed here, because a bake is not a frame.**
|
|
13634
14178
|
*
|
|
@@ -13657,12 +14201,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13657
14201
|
);
|
|
13658
14202
|
const levels = probeLevels(this.probeSize);
|
|
13659
14203
|
for (let level = 1; level < levels; level++) {
|
|
13660
|
-
/*
|
|
13661
|
-
const
|
|
13662
|
-
|
|
13663
|
-
|
|
13664
|
-
|
|
13665
|
-
|
|
14204
|
+
/* The whole cube at the level above, so the blur can cross a face edge, in a group kept. */
|
|
14205
|
+
const group = targets.chainGroup(
|
|
14206
|
+
this.probeMipLayout,
|
|
14207
|
+
this.probeMipUniforms.buffer,
|
|
14208
|
+
this.probeSampler,
|
|
14209
|
+
level,
|
|
14210
|
+
);
|
|
13666
14211
|
for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
|
|
13667
14212
|
const basis = PROBE_FACE_BASIS[face];
|
|
13668
14213
|
if (basis === undefined) continue;
|
|
@@ -13679,22 +14224,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13679
14224
|
this.probeMipUniforms.flush();
|
|
13680
14225
|
return;
|
|
13681
14226
|
}
|
|
13682
|
-
this.probeMipUniforms.writeFloats(slot, 0,
|
|
13683
|
-
this.probeMipUniforms.writeFloats(slot, 16,
|
|
13684
|
-
this.probeMipUniforms.writeFloats(slot, 32,
|
|
13685
|
-
this.probeMipUniforms.writeFloats(slot, 48,
|
|
14227
|
+
this.probeMipUniforms.writeFloats(slot, 0, padded(basis.basisX));
|
|
14228
|
+
this.probeMipUniforms.writeFloats(slot, 16, padded(basis.basisY));
|
|
14229
|
+
this.probeMipUniforms.writeFloats(slot, 32, padded(basis.forward));
|
|
14230
|
+
this.probeMipUniforms.writeFloats(slot, 48, ZERO4);
|
|
13686
14231
|
|
|
13687
14232
|
const pass = encoder.beginRenderPass({
|
|
13688
14233
|
label: `probe.mip${level}.face${face}`,
|
|
13689
14234
|
colorAttachments: [
|
|
13690
14235
|
{
|
|
13691
|
-
view:
|
|
13692
|
-
dimension: '2d',
|
|
13693
|
-
baseArrayLayer: face,
|
|
13694
|
-
arrayLayerCount: 1,
|
|
13695
|
-
baseMipLevel: level,
|
|
13696
|
-
mipLevelCount: 1,
|
|
13697
|
-
}),
|
|
14236
|
+
view: targets.chainTarget(level, face),
|
|
13698
14237
|
loadOp: 'clear',
|
|
13699
14238
|
storeOp: 'store',
|
|
13700
14239
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -13702,21 +14241,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13702
14241
|
],
|
|
13703
14242
|
});
|
|
13704
14243
|
pass.setPipeline(pipeline);
|
|
13705
|
-
pass.setBindGroup(
|
|
13706
|
-
0,
|
|
13707
|
-
device.createBindGroup({
|
|
13708
|
-
layout: this.probeMipLayout,
|
|
13709
|
-
entries: [
|
|
13710
|
-
{
|
|
13711
|
-
binding: 0,
|
|
13712
|
-
resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
|
|
13713
|
-
},
|
|
13714
|
-
{ binding: 1, resource: source },
|
|
13715
|
-
{ binding: 2, resource: this.probeSampler },
|
|
13716
|
-
],
|
|
13717
|
-
}),
|
|
13718
|
-
[slot],
|
|
13719
|
-
);
|
|
14244
|
+
pass.setBindGroup(0, group, [slot]);
|
|
13720
14245
|
pass.draw(3);
|
|
13721
14246
|
pass.end();
|
|
13722
14247
|
}
|
|
@@ -13742,8 +14267,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13742
14267
|
this.probePrefilterLayout,
|
|
13743
14268
|
this.pipelines.format,
|
|
13744
14269
|
);
|
|
13745
|
-
/* The whole chain, so a wide sample can select a level from it. */
|
|
13746
|
-
const
|
|
14270
|
+
/* The whole chain, so a wide sample can select a level from it, in a group kept. */
|
|
14271
|
+
const group = targets.prefilterGroupFor(
|
|
14272
|
+
this.probePrefilterLayout,
|
|
14273
|
+
this.probeMipUniforms.buffer,
|
|
14274
|
+
this.probeSampler,
|
|
14275
|
+
);
|
|
13747
14276
|
const offsets = PREFILTER_UNIFORM_OFFSETS;
|
|
13748
14277
|
const topLevel = levels - 1;
|
|
13749
14278
|
const samples = this.quality.environmentPrefilterSamples;
|
|
@@ -13751,13 +14280,20 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13751
14280
|
const irradianceLevel = irradianceLevelFor(edge);
|
|
13752
14281
|
const ggxTop = ggxMaxLevelFor(edge);
|
|
13753
14282
|
const boxChain = !this.quality.environmentPrefilter;
|
|
14283
|
+
/*
|
|
14284
|
+
* **The diffuse level of a layer the trace has written is the trace's**, so a rasterised bake
|
|
14285
|
+
* stops one level short of it. See `ProbeBaker.traced`: two writers of one level was light
|
|
14286
|
+
* sweeping down a courtyard a probe at a time with the clock paused.
|
|
14287
|
+
*/
|
|
14288
|
+
const lastLevel =
|
|
14289
|
+
this.probeBaker?.traced(layer) === true ? irradianceLevel - 1 : irradianceLevel;
|
|
13754
14290
|
|
|
13755
14291
|
/*
|
|
13756
14292
|
* **One draw a level, where the cube needed six.** An octahedral map has no faces, so the
|
|
13757
14293
|
* six-way loop is gone rather than moved, and a probe's convolution is six times fewer
|
|
13758
14294
|
* passes than the cube it replaces.
|
|
13759
14295
|
*/
|
|
13760
|
-
for (let level = 0; level <=
|
|
14296
|
+
for (let level = 0; level <= lastLevel; level++) {
|
|
13761
14297
|
const irradiance = level === irradianceLevel;
|
|
13762
14298
|
const roughness = irradiance ? 1 : roughnessForLevel(level, ggxTop);
|
|
13763
14299
|
const slot = this.probeMipUniforms.allocate();
|
|
@@ -13776,32 +14312,40 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13776
14312
|
/* Written at the generator's own offsets rather than at literals, because a roughness at
|
|
13777
14313
|
the wrong offset is a level convolved for some other roughness — which looks like a
|
|
13778
14314
|
plausible reflection and points at nothing. */
|
|
13779
|
-
this.probeMipUniforms.writeFloats(
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
this.probeMipUniforms.writeFloats(
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
14315
|
+
this.probeMipUniforms.writeFloats(
|
|
14316
|
+
slot,
|
|
14317
|
+
offsets.uPrefilterEdge.offset,
|
|
14318
|
+
one(Math.max(1, edge >> level)),
|
|
14319
|
+
);
|
|
14320
|
+
this.probeMipUniforms.writeFloats(
|
|
14321
|
+
slot,
|
|
14322
|
+
offsets.uPrefilterIrradiance.offset,
|
|
14323
|
+
one(irradiance ? 1 : 0),
|
|
14324
|
+
);
|
|
14325
|
+
this.probeMipUniforms.writeFloats(
|
|
14326
|
+
slot,
|
|
14327
|
+
offsets.uPrefilterBox.offset,
|
|
14328
|
+
one(boxChain ? 1 : 0),
|
|
14329
|
+
);
|
|
14330
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterLevel.offset, one(level));
|
|
14331
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterRoughness.offset, one(roughness));
|
|
14332
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSamples.offset, one(samples));
|
|
14333
|
+
this.probeMipUniforms.writeFloats(
|
|
14334
|
+
slot,
|
|
14335
|
+
offsets.uPrefilterSourceTexels.offset,
|
|
14336
|
+
one(this.probeSize),
|
|
14337
|
+
);
|
|
14338
|
+
this.probeMipUniforms.writeFloats(
|
|
14339
|
+
slot,
|
|
14340
|
+
offsets.uPrefilterSourceMaxLod.offset,
|
|
14341
|
+
one(topLevel),
|
|
14342
|
+
);
|
|
13793
14343
|
|
|
13794
14344
|
const pass = encoder.beginRenderPass({
|
|
13795
14345
|
label: `probe.prefilter${level}.layer${layer}`,
|
|
13796
14346
|
colorAttachments: [
|
|
13797
14347
|
{
|
|
13798
|
-
view:
|
|
13799
|
-
dimension: '2d',
|
|
13800
|
-
baseArrayLayer: layer,
|
|
13801
|
-
arrayLayerCount: 1,
|
|
13802
|
-
baseMipLevel: level,
|
|
13803
|
-
mipLevelCount: 1,
|
|
13804
|
-
}),
|
|
14348
|
+
view: targets.arrayView(prefilterTarget, layer, level),
|
|
13805
14349
|
loadOp: 'clear',
|
|
13806
14350
|
storeOp: 'store',
|
|
13807
14351
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -13809,21 +14353,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13809
14353
|
],
|
|
13810
14354
|
});
|
|
13811
14355
|
pass.setPipeline(prefilter);
|
|
13812
|
-
pass.setBindGroup(
|
|
13813
|
-
0,
|
|
13814
|
-
device.createBindGroup({
|
|
13815
|
-
layout: this.probePrefilterLayout,
|
|
13816
|
-
entries: [
|
|
13817
|
-
{
|
|
13818
|
-
binding: PREFILTER_UNIFORM_BINDING,
|
|
13819
|
-
resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
|
|
13820
|
-
},
|
|
13821
|
-
{ binding: PREFILTER_TEXTURE_BINDING, resource: source },
|
|
13822
|
-
{ binding: PREFILTER_SAMPLER_BINDING, resource: this.probeSampler },
|
|
13823
|
-
],
|
|
13824
|
-
}),
|
|
13825
|
-
[slot],
|
|
13826
|
-
);
|
|
14356
|
+
pass.setBindGroup(0, group, [slot]);
|
|
13827
14357
|
pass.draw(3);
|
|
13828
14358
|
pass.end();
|
|
13829
14359
|
}
|
|
@@ -14302,7 +14832,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14302
14832
|
f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.frameHeight);
|
|
14303
14833
|
/* In scene units, which is why an HDR target is what makes a threshold above 1 mean
|
|
14304
14834
|
anything: against a clamped buffer nothing is ever brighter than white. */
|
|
14305
|
-
f[slot(0) + prefilter('uThreshold')] = this.quality.bloomThreshold;
|
|
14835
|
+
f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
|
|
14306
14836
|
|
|
14307
14837
|
/* Each downsample's texel is its *source's*, which is the level above it. */
|
|
14308
14838
|
const down = (name: string): number => this.postField(BLOOM_DOWNSAMPLE_FIELDS, name);
|
|
@@ -14420,6 +14950,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14420
14950
|
|
|
14421
14951
|
this.fieldComposer?.dispose();
|
|
14422
14952
|
this.fieldComposer = null;
|
|
14953
|
+
this.driftIndex?.destroy();
|
|
14954
|
+
this.driftAtlas?.destroy();
|
|
14423
14955
|
|
|
14424
14956
|
this.pass = null;
|
|
14425
14957
|
this.encoder = null;
|
|
@@ -14427,6 +14959,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14427
14959
|
this.gpuTimer.dispose();
|
|
14428
14960
|
this.reflection?.dispose();
|
|
14429
14961
|
this.reflection = null;
|
|
14962
|
+
this.probeTargets?.dispose();
|
|
14963
|
+
this.exposurePass?.dispose();
|
|
14964
|
+
this.exposurePass = null;
|
|
14430
14965
|
this.surface.dispose();
|
|
14431
14966
|
}
|
|
14432
14967
|
}
|