@driftengine/core 4.2.1 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +272 -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/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +166 -1
- package/dist/render/backend/webgl2/renderer.js +424 -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 +12 -2
- 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 +176 -11
- package/dist/render/backend/webgpu/renderer.js +680 -250
- 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/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/agx.d.ts +31 -0
- package/dist/render/shaders/agx.js +81 -0
- package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
- package/dist/render/shaders/ambientOcclusion.js +120 -63
- 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 +1 -1
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
- 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/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +462 -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 +807 -271
- 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/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 +125 -63
- 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 +1 -1
- 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 {
|
|
@@ -150,7 +152,7 @@ import {
|
|
|
150
152
|
type GpuTextCube,
|
|
151
153
|
} from './textPass.ts';
|
|
152
154
|
import type { TextStyle } from '../../textLayout.ts';
|
|
153
|
-
import { deviceSnappedOrigin } from '../../textLayout.ts';
|
|
155
|
+
import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.ts';
|
|
154
156
|
import { GpuSurfaceTexture } from './surfaceTexturePass.ts';
|
|
155
157
|
import {
|
|
156
158
|
createSdfTextBindGroup,
|
|
@@ -178,6 +180,9 @@ 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 { clampAutoExposure } from '../../shaders/exposure.ts';
|
|
185
|
+
import { clampLocalExposure } from '../../shaders/localExposure.ts';
|
|
181
186
|
import type { UniformFields } from './scatterPass.ts';
|
|
182
187
|
import {
|
|
183
188
|
createDepthResolveLayout,
|
|
@@ -253,12 +258,15 @@ import {
|
|
|
253
258
|
identityGradeLut,
|
|
254
259
|
validateGradeLut,
|
|
255
260
|
} from '../../colourGrade.ts';
|
|
261
|
+
import {
|
|
262
|
+
FILM_LOOK_WITHOUT_COMPOSITE,
|
|
263
|
+
clampGrain,
|
|
264
|
+
clampVignette,
|
|
265
|
+
grainSeed,
|
|
266
|
+
} from '../../shaders/filmLook.ts';
|
|
256
267
|
import {
|
|
257
268
|
createProbeMipBindGroupLayout,
|
|
258
269
|
createProbePrefilterBindGroupLayout,
|
|
259
|
-
PREFILTER_SAMPLER_BINDING,
|
|
260
|
-
PREFILTER_TEXTURE_BINDING,
|
|
261
|
-
PREFILTER_UNIFORM_BINDING,
|
|
262
270
|
PREFILTER_UNIFORM_OFFSETS,
|
|
263
271
|
probeLevels,
|
|
264
272
|
probeMipPipeline,
|
|
@@ -339,6 +347,9 @@ import {
|
|
|
339
347
|
DEPTH_FRAG_SIZE,
|
|
340
348
|
DEPTH_FRAG_WGSL,
|
|
341
349
|
DEPTH_PREVIOUS_BINDING,
|
|
350
|
+
DEPTH_CUTOUT_VERT_FIELDS,
|
|
351
|
+
DEPTH_CUTOUT_VERT_SIZE,
|
|
352
|
+
DEPTH_INSTANCED_CUTOUT_VERT_FIELDS,
|
|
342
353
|
DEPTH_VERT_FIELDS,
|
|
343
354
|
DEPTH_VERT_SIZE,
|
|
344
355
|
SHADOW_FORMAT,
|
|
@@ -394,7 +405,6 @@ import {
|
|
|
394
405
|
} from './plumePass.ts';
|
|
395
406
|
import {
|
|
396
407
|
PARTICLE_VERT_FIELDS,
|
|
397
|
-
PARTICLE_VERT_SIZE,
|
|
398
408
|
createGpuParticles,
|
|
399
409
|
createParticleBindGroupLayout,
|
|
400
410
|
packInstances,
|
|
@@ -461,8 +471,12 @@ import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
|
|
|
461
471
|
import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.ts';
|
|
462
472
|
import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.ts';
|
|
463
473
|
import { DistanceFieldScene } from '../../gi/fieldScene.ts';
|
|
464
|
-
import { DEFAULT_FIELD_COMPOSE, FieldComposer } from './fieldCompose.ts';
|
|
474
|
+
import { DEFAULT_FIELD_COMPOSE, FieldComposer, MAX_FIELD_CASCADES } from './fieldCompose.ts';
|
|
475
|
+
import { cascadesForGrid } from '../../gi/fieldReach.ts';
|
|
465
476
|
import { ProbeBaker } from './probeBake.ts';
|
|
477
|
+
import type { ProbeBakeLightField } from './probeBake.ts';
|
|
478
|
+
import { createBounceLights, resolveBounceLights } from '../../gi/bounceLights.ts';
|
|
479
|
+
import { ProbeTargets } from './probeTargets.ts';
|
|
466
480
|
import { distanceFieldBounds } from '../../gi/fieldScene.ts';
|
|
467
481
|
import { fitProbeGrid } from '../../gi/probeGridFit.ts';
|
|
468
482
|
|
|
@@ -535,6 +549,11 @@ import type {
|
|
|
535
549
|
import type { Arena, FlushSchedule, ScheduledPass } from '../../frame/index.ts';
|
|
536
550
|
import { GpuInstancedBatch } from './instanced.ts';
|
|
537
551
|
import type { MeshInstances } from '../../instances.ts';
|
|
552
|
+
import { cutoutOf } from '../../cutoutCaster.ts';
|
|
553
|
+
import type { CutoutCaster } from '../../cutoutCaster.ts';
|
|
554
|
+
import { LightField } from '../../driftLight/lightField.ts';
|
|
555
|
+
import type { LightFieldOptions, LightFieldSource } from '../../driftLight/lightField.ts';
|
|
556
|
+
import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.ts';
|
|
538
557
|
|
|
539
558
|
/**
|
|
540
559
|
* How many draws one frame may make before the ring is full.
|
|
@@ -588,6 +607,12 @@ const MAX_SCATTER_DEPTH_DRAWS = 256;
|
|
|
588
607
|
* against the world.
|
|
589
608
|
*/
|
|
590
609
|
const MAX_OVERLAYS = 64;
|
|
610
|
+
/**
|
|
611
|
+
* Sky draws a frame can hold. A frame draws one, a mirror adds one, and a probe bake six a probe —
|
|
612
|
+
* given back after each probe's submit, so a whole grid needs six. Sixty-four is room for insets
|
|
613
|
+
* and passes nobody has written yet; the cost is sixteen kilobytes.
|
|
614
|
+
*/
|
|
615
|
+
const MAX_SKY_DRAWS = 64;
|
|
591
616
|
|
|
592
617
|
/**
|
|
593
618
|
* Distinct materials a frame may set.
|
|
@@ -1213,8 +1238,6 @@ type ShadowLayer = 'static' | 'static-peel' | 'dynamic';
|
|
|
1213
1238
|
|
|
1214
1239
|
/** The tint a draw gets when it asks for none, matching the WebGL2 path's reset-to-white. */
|
|
1215
1240
|
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
1241
|
/** A dust field standing still, for a light volume that asked for dust and not for wind. */
|
|
1219
1242
|
const NO_DRIFT = new Float32Array([0, 0, 0]);
|
|
1220
1243
|
/**
|
|
@@ -1265,6 +1288,24 @@ const NO_CLIP_PLANE = new Float32Array(4);
|
|
|
1265
1288
|
*/
|
|
1266
1289
|
const MOTION_CLIP_FLIP = mat4.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
1267
1290
|
|
|
1291
|
+
/* Scratch for the probe blur's and convolution's uniform writes, so a bake writes no array literal. */
|
|
1292
|
+
const SCRATCH_ONE = new Float32Array(1);
|
|
1293
|
+
const SCRATCH_VEC4 = new Float32Array(4);
|
|
1294
|
+
const ZERO4 = new Float32Array(4);
|
|
1295
|
+
/** `value` in a one-float scratch, overwritten by the next call. */
|
|
1296
|
+
function one(value: number): Float32Array {
|
|
1297
|
+
SCRATCH_ONE[0] = value;
|
|
1298
|
+
return SCRATCH_ONE;
|
|
1299
|
+
}
|
|
1300
|
+
/** A direction padded to a `vec4`, in a scratch overwritten by the next call. */
|
|
1301
|
+
function padded(direction: ArrayLike<number>): Float32Array {
|
|
1302
|
+
SCRATCH_VEC4[0] = direction[0] ?? 0;
|
|
1303
|
+
SCRATCH_VEC4[1] = direction[1] ?? 0;
|
|
1304
|
+
SCRATCH_VEC4[2] = direction[2] ?? 0;
|
|
1305
|
+
SCRATCH_VEC4[3] = 0;
|
|
1306
|
+
return SCRATCH_VEC4;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1268
1309
|
export class WebGPURenderer implements RendererApi {
|
|
1269
1310
|
/**
|
|
1270
1311
|
* Real timestamp queries, where the adapter offers them.
|
|
@@ -2081,13 +2122,21 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2081
2122
|
private readonly frameEye = new Float32Array(3);
|
|
2082
2123
|
/** The environment the frame was drawn with. See `bindMeshPass`. */
|
|
2083
2124
|
private frameEnv: Environment | null = null;
|
|
2125
|
+
/**
|
|
2126
|
+
* The sky this frame drew, or null until it draws one. A traced probe's escaping rays read it,
|
|
2127
|
+
* so a frame that stops drawing a sky must not go on lighting its probes with the last one.
|
|
2128
|
+
*/
|
|
2129
|
+
private frameSky: SkyColors | null = null;
|
|
2084
2130
|
/** That matrix with the clip-space correction folded in, rebuilt once a frame. */
|
|
2085
2131
|
private readonly correctedViewProj = new Float32Array(16);
|
|
2086
2132
|
|
|
2087
2133
|
private readonly skyLayout: GPUBindGroupLayout;
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2134
|
+
/**
|
|
2135
|
+
* A slot a sky draw, as every per-draw resource here is: see `UniformRing` and the 2026-08-27 rule.
|
|
2136
|
+
* It was one buffer rewritten at each draw, which gave every sky in an encoder the last camera.
|
|
2137
|
+
*/
|
|
2138
|
+
private readonly skyUniforms: UniformRing;
|
|
2139
|
+
private readonly skyFloats = new Float32Array(SKY_UNIFORM_SIZE / 4);
|
|
2091
2140
|
private readonly skyBindGroup: GPUBindGroup;
|
|
2092
2141
|
/** The camera's inverse with the clip correction undone, rebuilt once a frame. */
|
|
2093
2142
|
private readonly skyInvViewProj = new Float32Array(16);
|
|
@@ -2363,35 +2412,74 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2363
2412
|
* prefix that has already been uploaded costs one write of unchanged bytes and cannot be
|
|
2364
2413
|
* wrong: a ring only ever appends within a frame.
|
|
2365
2414
|
*/
|
|
2415
|
+
/**
|
|
2416
|
+
* Every per-draw ring the frame writes and flushes together, in one list.
|
|
2417
|
+
*
|
|
2418
|
+
* **One list, read by the flush and by the probe bake's rewind**, so a ring added to one cannot be
|
|
2419
|
+
* forgotten by the other: a ring flushed but never rewound would let a grid bake spend it, and one
|
|
2420
|
+
* rewound but never flushed would upload nothing. Built on first use, after the constructor has
|
|
2421
|
+
* set every field; `growTo` replaces a ring's buffer, never the ring, so the list stays true.
|
|
2422
|
+
*/
|
|
2423
|
+
private ringList: readonly UniformRing[] | null = null;
|
|
2424
|
+
private rings(): readonly UniformRing[] {
|
|
2425
|
+
this.ringList ??= [
|
|
2426
|
+
this.perDraw,
|
|
2427
|
+
this.lightVolumeVertices,
|
|
2428
|
+
this.lightVolumeFragments,
|
|
2429
|
+
this.plumeVerts,
|
|
2430
|
+
this.plumeFrags,
|
|
2431
|
+
this.waterVerts,
|
|
2432
|
+
this.waterFrags,
|
|
2433
|
+
this.boltVerts,
|
|
2434
|
+
this.boltFrags,
|
|
2435
|
+
this.causticsVerts,
|
|
2436
|
+
this.causticsFrags,
|
|
2437
|
+
this.flockVerts,
|
|
2438
|
+
this.flockFrags,
|
|
2439
|
+
this.windStreakVerts,
|
|
2440
|
+
this.windStreakFrags,
|
|
2441
|
+
this.scatterVerts,
|
|
2442
|
+
this.scatterFrags,
|
|
2443
|
+
this.perFrame,
|
|
2444
|
+
this.filmVerts,
|
|
2445
|
+
this.filmFrags,
|
|
2446
|
+
this.insetUniforms,
|
|
2447
|
+
this.panelVerts,
|
|
2448
|
+
this.panelFrags,
|
|
2449
|
+
this.textVerts,
|
|
2450
|
+
this.textFrags,
|
|
2451
|
+
this.sdfTextVerts,
|
|
2452
|
+
this.sdfTextFrags,
|
|
2453
|
+
this.lineVerts,
|
|
2454
|
+
this.lineFrags,
|
|
2455
|
+
this.skyUniforms,
|
|
2456
|
+
];
|
|
2457
|
+
return this.ringList;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2366
2460
|
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();
|
|
2461
|
+
for (const ring of this.rings()) ring.flush();
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/** Where every ring stands before work that submits an encoder of its own. */
|
|
2465
|
+
private readonly ringMarks: number[] = [];
|
|
2466
|
+
private markRings(): void {
|
|
2467
|
+
const rings = this.rings();
|
|
2468
|
+
this.ringMarks.length = rings.length;
|
|
2469
|
+
for (let i = 0; i < rings.length; i++) this.ringMarks[i] = (rings[i] as UniformRing).mark();
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* Give back every slot taken since `markRings`, once that work has been submitted.
|
|
2474
|
+
*
|
|
2475
|
+
* See `UniformRing.rewind` for why this is safe only after the submit. The palette ring goes back
|
|
2476
|
+
* with them, and the cached palette slot is dropped if it was one of those given back.
|
|
2477
|
+
*/
|
|
2478
|
+
private rewindRings(skinMark: number): void {
|
|
2479
|
+
const rings = this.rings();
|
|
2480
|
+
for (let i = 0; i < rings.length; i++) (rings[i] as UniformRing).rewind(this.ringMarks[i] ?? 0);
|
|
2481
|
+
this.skinPalettes.rewind(skinMark);
|
|
2482
|
+
if (this.skinPaletteSlot >= skinMark) this.skinPaletteSlot = -1;
|
|
2395
2483
|
}
|
|
2396
2484
|
|
|
2397
2485
|
/* -- The film ------------------------------------------------------------------------- */
|
|
@@ -2600,6 +2688,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2600
2688
|
private blankAlbedoBindGroup: GPUBindGroup;
|
|
2601
2689
|
/** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
|
|
2602
2690
|
private readonly uvScale = new Float32Array([1, 1]);
|
|
2691
|
+
/** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
|
|
2692
|
+
private materialDoubleSided = false;
|
|
2603
2693
|
|
|
2604
2694
|
createSurfaceTexture(
|
|
2605
2695
|
source: TexImageSource,
|
|
@@ -2664,6 +2754,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2664
2754
|
* a group holding the old view hands a destroyed texture to the next draw that asks.
|
|
2665
2755
|
*/
|
|
2666
2756
|
private forgetBindingsOf(texture: GpuSurfaceTexture): void {
|
|
2757
|
+
this.cutoutGroups.delete(texture);
|
|
2758
|
+
this.cutoutPeelGroups.delete(texture);
|
|
2667
2759
|
/* Same reasoning, for whichever SDF labels were bound against this atlas. */
|
|
2668
2760
|
this.sdfTextBindGroups.delete(texture);
|
|
2669
2761
|
this.flatBindGroups.clear();
|
|
@@ -2720,12 +2812,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2720
2812
|
const albedo = material?.albedo ?? null;
|
|
2721
2813
|
this.uvScale[0] = material?.uScale ?? 1;
|
|
2722
2814
|
this.uvScale[1] = material?.vScale ?? 1;
|
|
2815
|
+
this.materialDoubleSided = material?.doubleSided === true;
|
|
2723
2816
|
const normal = (material?.normal ?? null) as GpuSurfaceTexture | null;
|
|
2724
2817
|
const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
|
|
2725
2818
|
const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
|
|
2726
2819
|
this.material((f, i) => {
|
|
2727
2820
|
i[this.materialField('uAlbedoEnabled')] = albedo === null ? 0 : 1;
|
|
2728
2821
|
f[this.materialField('uAlbedoCutout')] = material?.cutout ?? 0;
|
|
2822
|
+
i[this.materialField('uDoubleSided')] = material?.doubleSided === true ? 1 : 0;
|
|
2729
2823
|
/* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
|
|
2730
2824
|
f[this.materialField('uNormalStrength')] =
|
|
2731
2825
|
normal === null ? 0 : (material?.normalStrength ?? 1);
|
|
@@ -3000,6 +3094,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3000
3094
|
text.setPlate(widthCells, heightCells, bottomCell);
|
|
3001
3095
|
}
|
|
3002
3096
|
|
|
3097
|
+
/** See `Renderer.snapTextCellSize`; only the drawing buffer differs. */
|
|
3098
|
+
snapTextCellSize(cellSize: number, viewportWidth: number): number {
|
|
3099
|
+
return deviceSnappedCellSize(cellSize, viewportWidth, this.surface.canvas.width);
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3003
3102
|
textWidth(text: GpuText, cellSize: number): number {
|
|
3004
3103
|
return text.layout.widthPx(cellSize);
|
|
3005
3104
|
}
|
|
@@ -3581,6 +3680,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3581
3680
|
timeSeconds: number,
|
|
3582
3681
|
): void {
|
|
3583
3682
|
if (!this.canDraw() || data.count === 0) return;
|
|
3683
|
+
if (batch.drawnInEncoder === this.encoderEpoch) {
|
|
3684
|
+
if (!this.warnedParticlesTwice) {
|
|
3685
|
+
this.warnedParticlesTwice = true;
|
|
3686
|
+
console.warn(
|
|
3687
|
+
'WebGPU: a particle batch was drawn twice into one encoder — a mirror and the view, or a ' +
|
|
3688
|
+
"probe's faces — and the second draw is skipped, because both would show the second " +
|
|
3689
|
+
"draw's particles. Give each view its own batch.",
|
|
3690
|
+
);
|
|
3691
|
+
}
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
batch.drawnInEncoder = this.encoderEpoch;
|
|
3584
3695
|
const live = packInstances(batch, data);
|
|
3585
3696
|
if (live === 0) return;
|
|
3586
3697
|
|
|
@@ -4359,7 +4470,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4359
4470
|
|
|
4360
4471
|
this.windStreakFragFloats.set(tint, (WIND_STREAK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
|
|
4361
4472
|
|
|
4362
|
-
const { queue } = this.surface.device;
|
|
4363
4473
|
this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
|
|
4364
4474
|
this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
|
|
4365
4475
|
|
|
@@ -4461,7 +4571,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4461
4571
|
|
|
4462
4572
|
this.flockFragFloats.set(tint, (FLOCK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
|
|
4463
4573
|
|
|
4464
|
-
const { queue } = this.surface.device;
|
|
4465
4574
|
this.flockVerts.writeBlock(vertexSlot, this.flockVertBlock);
|
|
4466
4575
|
this.flockFrags.writeBlock(fragmentSlot, this.flockFragBlock);
|
|
4467
4576
|
|
|
@@ -4814,7 +4923,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4814
4923
|
private readonly scatterLayout: GPUBindGroupLayout;
|
|
4815
4924
|
/** One slot per batch: the gust and the trample field belong to the batch, not the frame. */
|
|
4816
4925
|
private readonly scatterVerts: UniformRing;
|
|
4817
|
-
|
|
4926
|
+
/** A slot a draw, beside the vertex block's: see `createScatterBindGroupLayout` for why. */
|
|
4927
|
+
private readonly scatterFrags: UniformRing;
|
|
4818
4928
|
private readonly scatterFragStaging = new ArrayBuffer(SCATTER_FRAG_SIZE);
|
|
4819
4929
|
private readonly scatterFragFloats = new Float32Array(this.scatterFragStaging);
|
|
4820
4930
|
private readonly scatterFragInts = new Int32Array(this.scatterFragStaging);
|
|
@@ -5009,17 +5119,28 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5009
5119
|
private readonly dynamicView: GPUTextureView | null;
|
|
5010
5120
|
/** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
|
|
5011
5121
|
private readonly shadowPeelBindGroup: GPUBindGroup | null;
|
|
5122
|
+
/**
|
|
5123
|
+
* The cutout casters' layout, and a bind group per cutout map for each layer, built the first
|
|
5124
|
+
* time a map casts and kept: a `createBindGroup` per draw is the allocation the house rules forbid.
|
|
5125
|
+
* Cleared in `forgetBindingsOf`, because a group outliving its texture is a destroyed-texture error
|
|
5126
|
+
* at submit, and a texture's view is new when an update changes its size.
|
|
5127
|
+
*/
|
|
5128
|
+
private readonly shadowCutoutLayout: GPUBindGroupLayout;
|
|
5129
|
+
private readonly cutoutGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
|
|
5130
|
+
private readonly cutoutPeelGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
|
|
5131
|
+
private shadowPeelPassUniforms: GPUBuffer | null = null;
|
|
5132
|
+
private shadowPeelSampler: GPUSampler | null = null;
|
|
5012
5133
|
private readonly scatterDepthPeelBindGroup: GPUBindGroup | null;
|
|
5013
5134
|
/** Which layer the open pass is writing, so the sink picks the bindings that match it. */
|
|
5014
5135
|
private shadowLayerIsPeel = false;
|
|
5015
5136
|
/**
|
|
5016
5137
|
* Whether a peel pass has filled the map, which is what the frame may sample it on.
|
|
5017
5138
|
*
|
|
5018
|
-
* **Not whether the map exists.** `renderer.ts`
|
|
5019
|
-
* born
|
|
5020
|
-
*
|
|
5021
|
-
* static layer opens and set when a peel pass ends, which
|
|
5022
|
-
*
|
|
5139
|
+
* **Not whether the map exists.** `renderer.ts` asked that question, on the belief that a GL
|
|
5140
|
+
* texture is born reading as the far plane, and it is born zeroed: a scene that built a peel
|
|
5141
|
+
* map and never opened the layer had no sun at all under ANGLE's GL backend. It holds this
|
|
5142
|
+
* rule now as well. Cleared when the static layer opens and set when a peel pass ends, which
|
|
5143
|
+
* is per frame because every scene that peels does so every frame.
|
|
5023
5144
|
*/
|
|
5024
5145
|
private peelFilled = false;
|
|
5025
5146
|
/** One slot per caster, the same trick `perDraw` uses and for the same reason. */
|
|
@@ -5061,6 +5182,44 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5061
5182
|
* otherwise. A lookup rather than a rebind, because a `createBindGroup` per draw is the
|
|
5062
5183
|
* per-frame allocation the house rules forbid; both groups are built once at init.
|
|
5063
5184
|
*/
|
|
5185
|
+
/** The cutout bind group for `texture` in the open layer, built the first time it casts there. */
|
|
5186
|
+
private cutoutGroup(texture: GpuSurfaceTexture): GPUBindGroup | null {
|
|
5187
|
+
const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
|
|
5188
|
+
const groups = peel ? this.cutoutPeelGroups : this.cutoutGroups;
|
|
5189
|
+
const held = groups.get(texture);
|
|
5190
|
+
if (held !== undefined) return held;
|
|
5191
|
+
const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
|
|
5192
|
+
const previousView = peel ? this.shadowView : this.shadowBlankView;
|
|
5193
|
+
const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
|
|
5194
|
+
if (passUniforms === null || previousView === null || previousSampler === null) return null;
|
|
5195
|
+
const group = createDepthBindGroup(
|
|
5196
|
+
this.surface.device,
|
|
5197
|
+
this.shadowCutoutLayout,
|
|
5198
|
+
this.shadowDraws.buffer,
|
|
5199
|
+
passUniforms,
|
|
5200
|
+
previousView,
|
|
5201
|
+
previousSampler,
|
|
5202
|
+
null,
|
|
5203
|
+
{ view: texture.view, sampler: texture.sampler },
|
|
5204
|
+
);
|
|
5205
|
+
groups.set(texture, group);
|
|
5206
|
+
return group;
|
|
5207
|
+
}
|
|
5208
|
+
|
|
5209
|
+
/** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
|
|
5210
|
+
private writeCutout(
|
|
5211
|
+
slot: number,
|
|
5212
|
+
cutout: CutoutCaster<unknown>,
|
|
5213
|
+
fields: {
|
|
5214
|
+
readonly uUvScale: { readonly offset: number };
|
|
5215
|
+
readonly uAlphaCutout: { readonly offset: number };
|
|
5216
|
+
},
|
|
5217
|
+
): void {
|
|
5218
|
+
this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
|
|
5219
|
+
this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
|
|
5220
|
+
this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
|
|
5221
|
+
}
|
|
5222
|
+
|
|
5064
5223
|
private shadowGroup(): GPUBindGroup {
|
|
5065
5224
|
return this.shadowLayerIsPeel && this.shadowPeelBindGroup !== null
|
|
5066
5225
|
? this.shadowPeelBindGroup
|
|
@@ -5109,9 +5268,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5109
5268
|
* every frame would be an allocation in a per-frame path.
|
|
5110
5269
|
*/
|
|
5111
5270
|
private readonly casterSink: ShadowCasterSink = {
|
|
5112
|
-
mesh: (mesh, model) => {
|
|
5271
|
+
mesh: (mesh, model, material) => {
|
|
5113
5272
|
const pass = this.shadowPass;
|
|
5114
5273
|
if (pass === null) return;
|
|
5274
|
+
/* A cutout caster's group is looked up before a slot is spent, so a miss costs nothing. */
|
|
5275
|
+
const cutout = cutoutOf(material);
|
|
5276
|
+
const cutoutGroup =
|
|
5277
|
+
cutout === null ? null : this.cutoutGroup(cutout.albedo as unknown as GpuSurfaceTexture);
|
|
5115
5278
|
const slot = this.shadowDraws.allocate();
|
|
5116
5279
|
if (slot === null) return;
|
|
5117
5280
|
|
|
@@ -5122,20 +5285,28 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5122
5285
|
);
|
|
5123
5286
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model as Float32Array);
|
|
5124
5287
|
this.writeShadowWind(slot);
|
|
5288
|
+
if (cutout !== null && cutoutGroup !== null)
|
|
5289
|
+
this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
|
|
5125
5290
|
|
|
5126
5291
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
5127
5292
|
if (geometry.vertexBuffers === undefined) return;
|
|
5293
|
+
const cuts = cutoutGroup !== null;
|
|
5294
|
+
/* A two-sided caster casts from whichever face the light sees: nothing culled. */
|
|
5295
|
+
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
5128
5296
|
pass.setPipeline(
|
|
5129
5297
|
depthPipeline(
|
|
5130
5298
|
this.pipelines,
|
|
5131
5299
|
this.surface.device,
|
|
5132
|
-
this.shadowLayout,
|
|
5133
|
-
|
|
5300
|
+
cuts ? this.shadowCutoutLayout : this.shadowLayout,
|
|
5301
|
+
`${cuts ? 'depth-cutout' : 'depth'}|${cull}|${geometry.key ?? ''}`,
|
|
5134
5302
|
this.presentOf(geometry),
|
|
5135
|
-
|
|
5303
|
+
cull,
|
|
5304
|
+
false,
|
|
5305
|
+
false,
|
|
5306
|
+
cuts,
|
|
5136
5307
|
),
|
|
5137
5308
|
);
|
|
5138
|
-
pass.setBindGroup(0, this.shadowGroup(), [slot]);
|
|
5309
|
+
pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
|
|
5139
5310
|
for (let index = 0; index < geometry.vertexBuffers.length; index++) {
|
|
5140
5311
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
|
|
5141
5312
|
}
|
|
@@ -5146,7 +5317,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5146
5317
|
* A batch of rigid meshes into the depth map, placed by the same matrices the visible draw
|
|
5147
5318
|
* uses — one draw, however many instances, exactly as the colour pass does it.
|
|
5148
5319
|
*/
|
|
5149
|
-
instanced: (batch, data) => {
|
|
5320
|
+
instanced: (batch, data, material) => {
|
|
5150
5321
|
const pass = this.shadowPass;
|
|
5151
5322
|
if (pass === null) return;
|
|
5152
5323
|
const gpuBatch = batch as GpuInstancedBatch;
|
|
@@ -5154,6 +5325,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5154
5325
|
if (geometry.vertexBuffers === undefined) return;
|
|
5155
5326
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
5156
5327
|
if (count === 0) return;
|
|
5328
|
+
const cutout = cutoutOf(material);
|
|
5329
|
+
const cutoutGroup =
|
|
5330
|
+
cutout === null ? null : this.cutoutGroup(cutout.albedo as unknown as GpuSurfaceTexture);
|
|
5157
5331
|
|
|
5158
5332
|
const slot = this.shadowDraws.allocate();
|
|
5159
5333
|
if (slot === null) return;
|
|
@@ -5164,20 +5338,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5164
5338
|
DEPTH_VERT_FIELDS.uLightViewProj.offset,
|
|
5165
5339
|
this.correctedLightViewProj,
|
|
5166
5340
|
);
|
|
5341
|
+
if (cutout !== null && cutoutGroup !== null) {
|
|
5342
|
+
this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
|
|
5343
|
+
}
|
|
5167
5344
|
|
|
5345
|
+
const cuts = cutoutGroup !== null;
|
|
5346
|
+
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
5168
5347
|
pass.setPipeline(
|
|
5169
5348
|
depthPipeline(
|
|
5170
5349
|
this.pipelines,
|
|
5171
5350
|
this.surface.device,
|
|
5172
|
-
this.shadowLayout,
|
|
5173
|
-
|
|
5351
|
+
cuts ? this.shadowCutoutLayout : this.shadowLayout,
|
|
5352
|
+
`${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}|${cull}|${geometry.key ?? ''}`,
|
|
5174
5353
|
this.presentOf(geometry),
|
|
5175
|
-
|
|
5354
|
+
cull,
|
|
5176
5355
|
false,
|
|
5177
5356
|
true,
|
|
5357
|
+
cuts,
|
|
5178
5358
|
),
|
|
5179
5359
|
);
|
|
5180
|
-
pass.setBindGroup(0, this.shadowGroup(), [slot]);
|
|
5360
|
+
pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
|
|
5181
5361
|
let index = 0;
|
|
5182
5362
|
for (; index < geometry.vertexBuffers.length; index++) {
|
|
5183
5363
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
|
|
@@ -5676,34 +5856,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5676
5856
|
const blankView = blank.createView();
|
|
5677
5857
|
const blankSampler = device.createSampler({ label: 'flat.sampler' });
|
|
5678
5858
|
|
|
5679
|
-
/*
|
|
5680
|
-
* A one-texel cube for every point-shadow sampler the variant declares.
|
|
5681
|
-
*
|
|
5682
|
-
* Twelve of them exist in the `pointShadows` permutation and none has a map behind it
|
|
5683
|
-
* until row 3, but WebGPU requires every declared binding to be filled — and a cube view
|
|
5684
|
-
* needs six array layers, so the flat 1×1 stand-in above cannot serve.
|
|
5685
|
-
*
|
|
5686
|
-
* **White is the second guard, and it is a deliberate value rather than a spare one.** The
|
|
5687
|
-
* first is `uPointShadowIndex`, written to −1 for every light in `bindMeshPass`. If that
|
|
5688
|
-
* ever failed, a full-depth texel reads as an occluder at the far plane — `pointShadow`
|
|
5689
|
-
* converts the stored value back to a distance and 1.0 comes out as `far` — so the worst a
|
|
5690
|
-
* mis-selected light can do is find nothing in the way. Zeroes would have meant an
|
|
5691
|
-
* occluder pressed against the lamp, and every surface in the scene in shadow.
|
|
5692
|
-
*/
|
|
5693
|
-
const blankCube = device.createTexture({
|
|
5694
|
-
label: 'flat.emptyPointShadow',
|
|
5695
|
-
size: [1, 1, 6],
|
|
5696
|
-
format: 'rgba8unorm',
|
|
5697
|
-
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
5698
|
-
});
|
|
5699
|
-
device.queue.writeTexture(
|
|
5700
|
-
{ texture: blankCube },
|
|
5701
|
-
new Uint8Array(4 * 6).fill(255),
|
|
5702
|
-
{ bytesPerRow: 4, rowsPerImage: 1 },
|
|
5703
|
-
[1, 1, 6],
|
|
5704
|
-
);
|
|
5705
|
-
const blankCubeView = blankCube.createView({ dimension: 'cube' });
|
|
5706
|
-
|
|
5707
5859
|
/*
|
|
5708
5860
|
* A one-texel integer stand-in for the froxel table.
|
|
5709
5861
|
*
|
|
@@ -5753,6 +5905,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5753
5905
|
[1, 1, 1],
|
|
5754
5906
|
);
|
|
5755
5907
|
const blankArrayView = blankArray.createView({ dimension: '2d-array' });
|
|
5908
|
+
/*
|
|
5909
|
+
* DriftLight's two volumes before a field is whole: an index of one empty cell and an atlas
|
|
5910
|
+
* of one zero texel. Never read while `uDriftLight.x` is zero, and bound anyway, because a
|
|
5911
|
+
* group missing an entry its layout declares is a validation failure at `submit`.
|
|
5912
|
+
*/
|
|
5913
|
+
const blankVolume = (label: string, format: GPUTextureFormat): GPUTextureView =>
|
|
5914
|
+
device
|
|
5915
|
+
.createTexture({ label, size: [1, 1, 1], dimension: '3d', format, usage: 0x2 | 0x4 })
|
|
5916
|
+
.createView({ dimension: '3d' });
|
|
5917
|
+
const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
|
|
5918
|
+
const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
|
|
5756
5919
|
|
|
5757
5920
|
this.shadowMap = device.createTexture({
|
|
5758
5921
|
label: 'shadow.static',
|
|
@@ -5792,14 +5955,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5792
5955
|
this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
|
|
5793
5956
|
|
|
5794
5957
|
/*
|
|
5795
|
-
* **Cleared once, here, because a texture no pass has written
|
|
5958
|
+
* **Cleared once, here, because a texture no pass has written reads as zero**, which the
|
|
5959
|
+
* specification requires, and a zero in a map compared with `LEQUAL` is an occluder at the
|
|
5960
|
+
* light. A scene is entitled to switch directional shadows *on* and never open a pass —
|
|
5961
|
+
* `night-street` does exactly that, with `shadowStrength` at 0.85 and no `beginShadowPass`
|
|
5962
|
+
* anywhere in it — and sampling that zero shadows everything, which is a picture rather than
|
|
5963
|
+
* an error and reads as a lighting bug.
|
|
5796
5964
|
*
|
|
5797
|
-
*
|
|
5798
|
-
*
|
|
5799
|
-
* no such promise, and a scene is entitled to switch directional shadows *on* and never
|
|
5800
|
-
* open a pass — `night-street` does exactly that, with `shadowStrength` at 0.85 and no
|
|
5801
|
-
* `beginShadowPass` anywhere in it. Sampling garbage then shadows whatever the garbage
|
|
5802
|
-
* happens to say, which is a picture rather than an error and reads as a lighting bug.
|
|
5965
|
+
* `shadowMap.ts` does the same for the other backend, for the same reason found later: WebGL
|
|
5966
|
+
* zeroes a new texture too, and only ANGLE on Vulkan happened to hand one back reading 1.
|
|
5803
5967
|
*
|
|
5804
5968
|
* Found by dumping the flat block off both live renderers and comparing it field by field;
|
|
5805
5969
|
* `uPeeledShadowEnabled` came back 1 against 0 and pointed straight at the maps behind it.
|
|
@@ -5909,7 +6073,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5909
6073
|
* is writing, so the binding has to move whatever the shader intends to do with it.
|
|
5910
6074
|
*/
|
|
5911
6075
|
if (name === 'uEnvironment') {
|
|
5912
|
-
return this.probeView === null || !this.
|
|
6076
|
+
return this.probeView === null || !this.probeReadable()
|
|
5913
6077
|
? { view: blankArrayView, sampler: blankSampler }
|
|
5914
6078
|
: { view: this.probeView, sampler: this.probeSampler };
|
|
5915
6079
|
}
|
|
@@ -5943,6 +6107,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5943
6107
|
if (name === 'uCookieAtlas') {
|
|
5944
6108
|
return { view: this.cookieView ?? blankView, sampler: iesSampler };
|
|
5945
6109
|
}
|
|
6110
|
+
/*
|
|
6111
|
+
* DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
|
|
6112
|
+
* sampler the way the froxel table does; and its atlas, which is filtered inside a brick and
|
|
6113
|
+
* so takes the linear, clamped one the photometric atlas uses.
|
|
6114
|
+
*/
|
|
6115
|
+
if (name === 'uDriftLightIndex') {
|
|
6116
|
+
return { view: this.driftIndexView ?? blankDriftIndexView, sampler: shadowSampler };
|
|
6117
|
+
}
|
|
6118
|
+
if (name === 'uDriftLightAtlas') {
|
|
6119
|
+
return { view: this.driftAtlasView ?? blankDriftAtlasView, sampler: iesSampler };
|
|
6120
|
+
}
|
|
5946
6121
|
/*
|
|
5947
6122
|
* The colour the frame had already drawn, or the blank texel a frame that refracts nothing
|
|
5948
6123
|
* falls back to.
|
|
@@ -5998,12 +6173,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5998
6173
|
this.blankAlbedoBindGroup = this.bindGroup;
|
|
5999
6174
|
|
|
6000
6175
|
this.skyLayout = createSkyBindGroupLayout(device);
|
|
6001
|
-
this.skyUniforms =
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6176
|
+
this.skyUniforms = new UniformRing(
|
|
6177
|
+
device,
|
|
6178
|
+
SKY_UNIFORM_SIZE,
|
|
6179
|
+
MAX_SKY_DRAWS,
|
|
6180
|
+
USAGE_UNIFORM_DST,
|
|
6181
|
+
'sky.ring',
|
|
6182
|
+
);
|
|
6183
|
+
this.skyBindGroup = createSkyBindGroup(device, this.skyLayout, this.skyUniforms.buffer);
|
|
6007
6184
|
|
|
6008
6185
|
this.plumeLayout = createPlumeBindGroupLayout(device);
|
|
6009
6186
|
this.plumeVerts = new UniformRing(
|
|
@@ -6082,6 +6259,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6082
6259
|
usage: 0x10 | 0x4 | 0x2, // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_DST
|
|
6083
6260
|
})
|
|
6084
6261
|
: null;
|
|
6262
|
+
this.probeTargets =
|
|
6263
|
+
this.probe === null ? null : new ProbeTargets(device, this.probe, this.probeSize);
|
|
6085
6264
|
/*
|
|
6086
6265
|
* The prefiltered twin, allocated on exactly the condition the capture is and with exactly
|
|
6087
6266
|
* its format and level count. Same format because a pipeline's colour target must equal its
|
|
@@ -6124,6 +6303,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6124
6303
|
is not a screen-sized target. Filtering is what makes a 32-lattice table smooth rather
|
|
6125
6304
|
than banded, so it is declared filterable like the colour inputs and unlike the depth. */
|
|
6126
6305
|
{ binding: RUSH_TEXTURES.uGradeLut, dimension: '3d' },
|
|
6306
|
+
/* Eye adaptation's held brightness: one half-float texel, or the scene where none is held. */
|
|
6307
|
+
{ binding: RUSH_TEXTURES.uExposureHeld },
|
|
6308
|
+
/* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
|
|
6309
|
+
{ binding: RUSH_TEXTURES.uExposureLocal },
|
|
6127
6310
|
]);
|
|
6128
6311
|
this.rushUniforms = device.createBuffer({
|
|
6129
6312
|
label: 'post.rushUniforms',
|
|
@@ -6353,23 +6536,27 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6353
6536
|
MAX_PLUMES_PER_FRAME,
|
|
6354
6537
|
USAGE_UNIFORM_DST,
|
|
6355
6538
|
);
|
|
6356
|
-
this.
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6539
|
+
this.scatterFrags = new UniformRing(
|
|
6540
|
+
device,
|
|
6541
|
+
SCATTER_FRAG_SIZE,
|
|
6542
|
+
MAX_PLUMES_PER_FRAME,
|
|
6543
|
+
USAGE_UNIFORM_DST,
|
|
6544
|
+
'scatter.fragRing',
|
|
6545
|
+
);
|
|
6361
6546
|
this.scatterBindGroup = createScatterBindGroup(
|
|
6362
6547
|
device,
|
|
6363
6548
|
this.scatterLayout,
|
|
6364
6549
|
this.scatterVerts.buffer,
|
|
6365
|
-
this.
|
|
6550
|
+
this.scatterFrags.buffer,
|
|
6366
6551
|
);
|
|
6367
6552
|
|
|
6368
6553
|
this.shadowLayout = createDepthBindGroupLayout(device);
|
|
6369
6554
|
this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
|
|
6555
|
+
this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
|
|
6370
6556
|
this.shadowDraws = new UniformRing(
|
|
6371
6557
|
device,
|
|
6372
|
-
|
|
6558
|
+
/* The cutout block is the plain one and two fields more, and a slot must hold either. */
|
|
6559
|
+
Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE),
|
|
6373
6560
|
MAX_DRAWS_PER_FRAME,
|
|
6374
6561
|
USAGE_UNIFORM_DST,
|
|
6375
6562
|
'shadow.drawRing',
|
|
@@ -6441,6 +6628,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6441
6628
|
if (peelPassUniforms !== null) {
|
|
6442
6629
|
device.queue.writeBuffer(peelPassUniforms, 0, new Int32Array([1]));
|
|
6443
6630
|
}
|
|
6631
|
+
this.shadowPeelPassUniforms = peelPassUniforms;
|
|
6632
|
+
this.shadowPeelSampler = shadowSampler;
|
|
6444
6633
|
this.shadowPeelBindGroup =
|
|
6445
6634
|
peelPassUniforms === null
|
|
6446
6635
|
? null
|
|
@@ -6465,15 +6654,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6465
6654
|
shadowSampler,
|
|
6466
6655
|
);
|
|
6467
6656
|
|
|
6468
|
-
/*
|
|
6469
|
-
* The light volume, last because it wants the shadow map and the stand-in that everything
|
|
6470
|
-
* above already built. The peel and dynamic maps get the white one-pixel texture, which is
|
|
6471
|
-
* depth 1 and therefore nothing in the way — see `createLightVolumeBindGroup` for why a
|
|
6472
|
-
* stand-in of zero would put every beam in shadow instead.
|
|
6473
|
-
*/
|
|
6474
|
-
const uniformBuffer = (label: string, size: number): GPUBuffer =>
|
|
6475
|
-
device.createBuffer({ label, size, usage: USAGE_UNIFORM_DST });
|
|
6476
|
-
|
|
6477
6657
|
/*
|
|
6478
6658
|
* The overlays. Rings rather than single blocks because a frame draws several of each —
|
|
6479
6659
|
* the loader alone is two panels and two strings — and `queue.writeBuffer` does not
|
|
@@ -6796,6 +6976,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6796
6976
|
this.reflectionQueue.reset();
|
|
6797
6977
|
/* Redeclared every frame, so a field a consumer stops declaring stops lighting. */
|
|
6798
6978
|
this.distanceFields.reset();
|
|
6979
|
+
this.frameSky = null;
|
|
6799
6980
|
this.oitActive =
|
|
6800
6981
|
this.quality.orderIndependent && this.quality.screenEffects && this.samples === 1;
|
|
6801
6982
|
/* Read off the line above rather than restating its last term: with the other two true, the
|
|
@@ -6818,6 +6999,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6818
6999
|
this.flushOverlay();
|
|
6819
7000
|
|
|
6820
7001
|
this.framePresented = false;
|
|
7002
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
6821
7003
|
/* Before the budget is cleared, because what to grow to is what the last frame asked for. */
|
|
6822
7004
|
this.growRings();
|
|
6823
7005
|
this.budget.reset();
|
|
@@ -6857,6 +7039,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6857
7039
|
this.windStreakVerts.reset();
|
|
6858
7040
|
this.windStreakFrags.reset();
|
|
6859
7041
|
this.scatterVerts.reset();
|
|
7042
|
+
this.scatterFrags.reset();
|
|
6860
7043
|
this.panelVerts.reset();
|
|
6861
7044
|
this.panelFrags.reset();
|
|
6862
7045
|
this.filmVerts.reset();
|
|
@@ -6868,6 +7051,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6868
7051
|
this.sdfTextFrags.reset();
|
|
6869
7052
|
this.lineVerts.reset();
|
|
6870
7053
|
this.lineFrags.reset();
|
|
7054
|
+
this.skyUniforms.reset();
|
|
6871
7055
|
/* The material ring, and the open slot with it: last frame's slots are gone. */
|
|
6872
7056
|
this.perFrame.reset();
|
|
6873
7057
|
this.materials.dirty();
|
|
@@ -7464,9 +7648,29 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7464
7648
|
f[base + 1] = counts[1];
|
|
7465
7649
|
f[base + 2] = counts[2];
|
|
7466
7650
|
}
|
|
7651
|
+
/* Which sets the shading reads, and how far between them. See `probeSweeps.ts`. */
|
|
7652
|
+
const setsAt = this.fragment.fields['uProbeGridSets'];
|
|
7653
|
+
if (setsAt !== undefined) {
|
|
7654
|
+
const sets = this.probeSweeps?.uniforms(this.probeSets) ?? this.probeSets.fill(0);
|
|
7655
|
+
const base = setsAt.offset / 4;
|
|
7656
|
+
f[base] = sets[0] ?? 0;
|
|
7657
|
+
f[base + 1] = sets[1] ?? 0;
|
|
7658
|
+
f[base + 2] = sets[2] ?? 0;
|
|
7659
|
+
}
|
|
7467
7660
|
/* Which chain is bound, because the level and the weight both depend on it. */
|
|
7468
7661
|
f[at('uEnvironmentPrefiltered')] = this.quality.environmentPrefilter ? 1 : 0;
|
|
7469
7662
|
}
|
|
7663
|
+
/* DriftLight, or zeros that switch it off: see `driftLight/uniforms.ts`. */
|
|
7664
|
+
this.uploadLightField();
|
|
7665
|
+
const drift = this.driftUniforms;
|
|
7666
|
+
resolveDriftLight(
|
|
7667
|
+
this.driftAtlas === null ? null : this.lightField,
|
|
7668
|
+
this.probePassActive,
|
|
7669
|
+
camera.position,
|
|
7670
|
+
drift,
|
|
7671
|
+
);
|
|
7672
|
+
f.set(drift.light, at('uDriftLight'));
|
|
7673
|
+
f.set(drift.origin, at('uDriftLightOrigin'));
|
|
7470
7674
|
/*
|
|
7471
7675
|
* The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
|
|
7472
7676
|
*
|
|
@@ -7544,7 +7748,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7544
7748
|
* it every frame.
|
|
7545
7749
|
*/
|
|
7546
7750
|
if (this.probeEnabled) {
|
|
7547
|
-
const usable = this.
|
|
7751
|
+
const usable = this.probeReadable();
|
|
7548
7752
|
f[at('uEnvironmentEnabled')] = usable ? 1 : 0;
|
|
7549
7753
|
f[at('uEnvironmentMaxLod')] = this.probeMaxLod;
|
|
7550
7754
|
/*
|
|
@@ -7803,6 +8007,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7803
8007
|
|
|
7804
8008
|
/** Said once rather than every frame, for the reason `warnedFull` gives. */
|
|
7805
8009
|
private warnedMaterialsFull = false;
|
|
8010
|
+
private warnedSkyFull = false;
|
|
8011
|
+
private warnedParticlesTwice = false;
|
|
8012
|
+
/**
|
|
8013
|
+
* Which encoder draws are being recorded into, by count: a new one at every `beginFrame`, and one
|
|
8014
|
+
* of its own for each probe bake, restored after. What a batch drawn per encoder is checked by.
|
|
8015
|
+
*/
|
|
8016
|
+
private encoderEpoch = 0;
|
|
8017
|
+
private encoderEpochs = 0;
|
|
7806
8018
|
|
|
7807
8019
|
/**
|
|
7808
8020
|
* Draw one mesh.
|
|
@@ -8116,7 +8328,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8116
8328
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
8117
8329
|
/* The two order-independent buffers are two formats and two blend states, so a draw into
|
|
8118
8330
|
one must never share a cached pipeline with the same mesh drawn into the other. */
|
|
8119
|
-
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}
|
|
8331
|
+
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
|
|
8332
|
+
/* Culling is pipeline state here: a two-sided surface culls nothing. */
|
|
8333
|
+
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
8120
8334
|
|
|
8121
8335
|
/*
|
|
8122
8336
|
* The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
|
|
@@ -8161,6 +8375,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8161
8375
|
layer,
|
|
8162
8376
|
false,
|
|
8163
8377
|
this.oitMode,
|
|
8378
|
+
this.materialDoubleSided,
|
|
8164
8379
|
);
|
|
8165
8380
|
}
|
|
8166
8381
|
|
|
@@ -8394,7 +8609,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8394
8609
|
plain one would hand a draw the wrong layout. */
|
|
8395
8610
|
const key =
|
|
8396
8611
|
`${base}|inst${blend ? '|blend' : ''}` +
|
|
8397
|
-
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}
|
|
8612
|
+
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
8613
|
+
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
8398
8614
|
|
|
8399
8615
|
const pipelines = this.targetPipelines();
|
|
8400
8616
|
let pipeline = pipelines.peek(key);
|
|
@@ -8416,6 +8632,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8416
8632
|
depthWrite,
|
|
8417
8633
|
layer,
|
|
8418
8634
|
true,
|
|
8635
|
+
'none',
|
|
8636
|
+
this.materialDoubleSided,
|
|
8419
8637
|
);
|
|
8420
8638
|
}
|
|
8421
8639
|
|
|
@@ -8576,7 +8794,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8576
8794
|
f[atF('uFogNear')] = medium.fogNear;
|
|
8577
8795
|
f[atF('uFogFar')] = medium.fogFar;
|
|
8578
8796
|
|
|
8579
|
-
this.
|
|
8797
|
+
const fragmentSlot = this.scatterFrags.allocate();
|
|
8798
|
+
if (fragmentSlot === null) return;
|
|
8799
|
+
this.scatterFrags.writeBlock(fragmentSlot, this.scatterFragInts);
|
|
8580
8800
|
|
|
8581
8801
|
const scatterPipe = scatterPipeline(this.pipelines, this.surface.device, this.scatterLayout);
|
|
8582
8802
|
/* One dynamic offset, and a group of two uniform buffers: nothing sampled, nothing read. */
|
|
@@ -8585,7 +8805,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8585
8805
|
scatterCommand.pipeline = scatterPipe;
|
|
8586
8806
|
scatterCommand.bindGroup = this.scatterBindGroup;
|
|
8587
8807
|
scatterCommand.offsetA = slot;
|
|
8588
|
-
scatterCommand.
|
|
8808
|
+
scatterCommand.offsetB = fragmentSlot;
|
|
8809
|
+
scatterCommand.offsetCount = 2;
|
|
8589
8810
|
for (let index = 0; index < scatter.vertexBuffers.length; index++) {
|
|
8590
8811
|
scatterCommand.vertexBuffers[index] = scatter.vertexBuffers[index] as GPUBuffer;
|
|
8591
8812
|
}
|
|
@@ -8598,7 +8819,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8598
8819
|
const pass = this.openPass();
|
|
8599
8820
|
if (pass === null) return;
|
|
8600
8821
|
pass.setPipeline(scatterPipe);
|
|
8601
|
-
pass.setBindGroup(0, this.scatterBindGroup, [slot]);
|
|
8822
|
+
pass.setBindGroup(0, this.scatterBindGroup, [slot, fragmentSlot]);
|
|
8602
8823
|
for (let index = 0; index < scatter.vertexBuffers.length; index++) {
|
|
8603
8824
|
pass.setVertexBuffer(index, scatter.vertexBuffers[index] as GPUBuffer);
|
|
8604
8825
|
}
|
|
@@ -9259,6 +9480,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9259
9480
|
*/
|
|
9260
9481
|
private rebuildFlatGroupsForNewRings(): void {
|
|
9261
9482
|
this.flatBindGroups.clear();
|
|
9483
|
+
this.flatEnvironment = this.flatTextures('uEnvironment').view;
|
|
9262
9484
|
this.skinnedGroups.length = 0;
|
|
9263
9485
|
|
|
9264
9486
|
const albedo = this.albedo;
|
|
@@ -9285,6 +9507,27 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9285
9507
|
this.materials.dirty();
|
|
9286
9508
|
}
|
|
9287
9509
|
|
|
9510
|
+
/**
|
|
9511
|
+
* The environment view the cached flat groups bind: the grid, or the blank stand-in while the
|
|
9512
|
+
* grid may not be read. What `rebindEnvironment` compares against.
|
|
9513
|
+
*/
|
|
9514
|
+
private flatEnvironment: GPUTextureView | null = null;
|
|
9515
|
+
|
|
9516
|
+
/**
|
|
9517
|
+
* Rebuild the flat groups if, and only if, the environment they bind has changed.
|
|
9518
|
+
*
|
|
9519
|
+
* **Every cached group holds the environment view**, so a change of it makes all of them stale;
|
|
9520
|
+
* but nothing else a bake does touches a group. Rebuilding on every bake's two edges regardless
|
|
9521
|
+
* emptied the cache twice a bake, and every material built its group again inside the bake and
|
|
9522
|
+
* again after it: a courtyard re-baking a few faces a frame spent 28% of its CPU there on the
|
|
9523
|
+
* native host, where a `createBindGroup` costs most. What would make this wrong is a second
|
|
9524
|
+
* binding that a bake changes, which would have to be compared here too.
|
|
9525
|
+
*/
|
|
9526
|
+
private rebindEnvironment(): void {
|
|
9527
|
+
if (this.flatTextures('uEnvironment').view !== this.flatEnvironment)
|
|
9528
|
+
this.rebuildFlatBindGroup();
|
|
9529
|
+
}
|
|
9530
|
+
|
|
9288
9531
|
/**
|
|
9289
9532
|
* Rebuild the group for the open material, and drop every cached one.
|
|
9290
9533
|
*
|
|
@@ -9296,6 +9539,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9296
9539
|
*/
|
|
9297
9540
|
private rebuildFlatBindGroup(): void {
|
|
9298
9541
|
this.flatBindGroups.clear();
|
|
9542
|
+
this.flatEnvironment = this.flatTextures('uEnvironment').view;
|
|
9299
9543
|
if (
|
|
9300
9544
|
this.albedo === null &&
|
|
9301
9545
|
this.normalMap === null &&
|
|
@@ -9336,6 +9580,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9336
9580
|
*/
|
|
9337
9581
|
drawSky(camera: Camera, sky: SkyColors, env: Environment): void {
|
|
9338
9582
|
if (!this.canDraw()) return;
|
|
9583
|
+
this.frameSky = sky;
|
|
9339
9584
|
|
|
9340
9585
|
const f = this.skyFloats;
|
|
9341
9586
|
const at = (name: keyof typeof SKY_FIELDS): number => SKY_FIELDS[name].offset / 4;
|
|
@@ -9370,7 +9615,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9370
9615
|
f.set(medium.underwaterColor, at('uUnderwaterColor'));
|
|
9371
9616
|
f[at('uUnderwaterFactor')] = medium.underwaterFactor;
|
|
9372
9617
|
|
|
9373
|
-
this.
|
|
9618
|
+
const slot = this.skyUniforms.allocate();
|
|
9619
|
+
if (slot === null) {
|
|
9620
|
+
if (!this.warnedSkyFull) {
|
|
9621
|
+
this.warnedSkyFull = true;
|
|
9622
|
+
console.warn(
|
|
9623
|
+
`WebGPU: more than ${MAX_SKY_DRAWS} sky draws in a frame; the rest are skipped`,
|
|
9624
|
+
);
|
|
9625
|
+
}
|
|
9626
|
+
return;
|
|
9627
|
+
}
|
|
9628
|
+
this.skyUniforms.writeFloats(slot, 0, f);
|
|
9374
9629
|
|
|
9375
9630
|
const skyPipe = skyPipeline(this.pipelines, this.surface.device, this.skyLayout);
|
|
9376
9631
|
/* Reads nothing: the group is one uniform buffer, and the sky samples no attachment. */
|
|
@@ -9378,12 +9633,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9378
9633
|
if (skyCommand !== null) {
|
|
9379
9634
|
skyCommand.pipeline = skyPipe;
|
|
9380
9635
|
skyCommand.bindGroup = this.skyBindGroup;
|
|
9636
|
+
skyCommand.offsetA = slot;
|
|
9637
|
+
skyCommand.offsetCount = 1;
|
|
9381
9638
|
skyCommand.count = 3;
|
|
9382
9639
|
} else {
|
|
9383
9640
|
const pass = this.openPass();
|
|
9384
9641
|
if (pass === null) return;
|
|
9385
9642
|
pass.setPipeline(skyPipe);
|
|
9386
|
-
pass.setBindGroup(0, this.skyBindGroup);
|
|
9643
|
+
pass.setBindGroup(0, this.skyBindGroup, [slot]);
|
|
9387
9644
|
pass.draw(3);
|
|
9388
9645
|
}
|
|
9389
9646
|
}
|
|
@@ -9494,6 +9751,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9494
9751
|
if (aoStrength > 0 && this.frameProjection !== null) this.runOcclusion(encoder);
|
|
9495
9752
|
const bloomStrength = this.quality.bloom * this.bloomScale;
|
|
9496
9753
|
if (bloomStrength > 0) this.runBloom(encoder);
|
|
9754
|
+
/*
|
|
9755
|
+
* Eye adaptation, metered from the scene the composite shows, and local exposure's grid beside
|
|
9756
|
+
* it. The held brightness is kept for either, since local exposure moves regions relative to it.
|
|
9757
|
+
* See `setAutoExposure` and `setLocalExposure`.
|
|
9758
|
+
*/
|
|
9759
|
+
const localising = this.localExposure > 0;
|
|
9760
|
+
const measuring = (this.autoExposure > 0 || localising) && this.exposurePass !== null;
|
|
9761
|
+
const adapting = measuring && this.autoExposure > 0;
|
|
9762
|
+
const shownScene = this.reconstructing ? this.reconShownView : this.sceneColorView;
|
|
9763
|
+
if (measuring && shownScene !== null) {
|
|
9764
|
+
this.exposurePass?.run(encoder, shownScene, this.autoExposureDt, localising);
|
|
9765
|
+
}
|
|
9497
9766
|
|
|
9498
9767
|
/* The grade travels here only where the mesh pass gave it up. See `bindMeshPass`. */
|
|
9499
9768
|
const graded = this.quality.hdrScene;
|
|
@@ -9533,6 +9802,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9533
9802
|
/* The grade, sampled after the curve and before the veil. See `uGradeLut` in `rush.ts`. */
|
|
9534
9803
|
f[at('uGradeStrength')] = this.gradeStrength;
|
|
9535
9804
|
f[at('uGradeSize')] = this.gradeSize;
|
|
9805
|
+
/* The lens and the print. See `setVignette` and `setFilmGrain`. */
|
|
9806
|
+
f[at('uVignette')] = this.vignette;
|
|
9807
|
+
f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
|
|
9808
|
+
f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
|
|
9809
|
+
f[at('uGrain')] = this.grain;
|
|
9810
|
+
f[at('uGrain') + 1] = this.grainSeed;
|
|
9536
9811
|
device.queue.writeBuffer(this.rushUniforms, 0, this.rushStaging);
|
|
9537
9812
|
|
|
9538
9813
|
const pass = encoder.beginRenderPass({
|
|
@@ -9615,6 +9890,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9615
9890
|
a frame that never grades anything should not carry a texture, and a declared binding
|
|
9616
9891
|
must still have a complete one — the argument the bloom fallback above makes. */
|
|
9617
9892
|
...entry(RUSH_TEXTURES.uGradeLut, this.ensureGradeView()),
|
|
9893
|
+
/* The scene as the placeholder until adaptation is asked for: the strength is zero then,
|
|
9894
|
+
and a declared binding still needs a complete texture, as the bloom fallback says. */
|
|
9895
|
+
...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
|
|
9896
|
+
...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
|
|
9618
9897
|
],
|
|
9619
9898
|
layout: this.rushLayout,
|
|
9620
9899
|
});
|
|
@@ -11504,8 +11783,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11504
11783
|
const swap = this.swapView();
|
|
11505
11784
|
if (swap === null) return null;
|
|
11506
11785
|
|
|
11507
|
-
|
|
11508
|
-
|
|
11786
|
+
/*
|
|
11787
|
+
* **The drawing buffer's size, not the scene depth's.** The overlay draws on the swap image,
|
|
11788
|
+
* past the resolve, and a reconstruction draws the scene depth small: sized from that, the
|
|
11789
|
+
* device refused the pass beside a full-size swap image and every overlay went missing.
|
|
11790
|
+
*/
|
|
11791
|
+
const width = Math.max(1, this.surface.canvas.width);
|
|
11792
|
+
const height = Math.max(1, this.surface.canvas.height);
|
|
11509
11793
|
if (
|
|
11510
11794
|
this.overlayDepth === null ||
|
|
11511
11795
|
this.overlayDepth.width !== width ||
|
|
@@ -11863,9 +12147,31 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11863
12147
|
* had stopped declaring. What the guard is actually for is not *building* a composer for a
|
|
11864
12148
|
* profile that never declares anything, so that is all it now guards.
|
|
11865
12149
|
*/
|
|
12150
|
+
if (this.fieldComposer === null && this.distanceFields.length === 0) return;
|
|
12151
|
+
/*
|
|
12152
|
+
* **As many cascades as reach the probe grid**, and rebuilt when the grid asks for a different
|
|
12153
|
+
* number: the far end of a courtyard longer than the default 16 m was traced against no field
|
|
12154
|
+
* at all and lit as open sky from every side. See `gi/fieldReach.ts`.
|
|
12155
|
+
*/
|
|
12156
|
+
const grid = this.indirectGrid();
|
|
12157
|
+
const cascades =
|
|
12158
|
+
grid === null
|
|
12159
|
+
? DEFAULT_FIELD_COMPOSE.cascades
|
|
12160
|
+
: cascadesForGrid(
|
|
12161
|
+
grid,
|
|
12162
|
+
DEFAULT_FIELD_COMPOSE.radius,
|
|
12163
|
+
DEFAULT_FIELD_COMPOSE.cascades,
|
|
12164
|
+
MAX_FIELD_CASCADES,
|
|
12165
|
+
);
|
|
12166
|
+
if (this.fieldComposer !== null && this.fieldComposer.cascadeCount !== cascades) {
|
|
12167
|
+
this.fieldComposer.dispose();
|
|
12168
|
+
this.fieldComposer = null;
|
|
12169
|
+
}
|
|
11866
12170
|
if (this.fieldComposer === null) {
|
|
11867
|
-
|
|
11868
|
-
|
|
12171
|
+
this.fieldComposer = new FieldComposer(this.surface.device, {
|
|
12172
|
+
...DEFAULT_FIELD_COMPOSE,
|
|
12173
|
+
cascades,
|
|
12174
|
+
});
|
|
11869
12175
|
}
|
|
11870
12176
|
this.fieldComposer.compose(encoder, this.distanceFields, this.frameEye);
|
|
11871
12177
|
}
|
|
@@ -11933,12 +12239,34 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11933
12239
|
* never said what was lighting it.
|
|
11934
12240
|
*/
|
|
11935
12241
|
const env = this.frameEnv;
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
12242
|
+
/*
|
|
12243
|
+
* And the scene's summed fixed lights, which the trace adds to what a hit receives: the bounce
|
|
12244
|
+
* of every candle, not only of the ones near enough to be shaded exactly.
|
|
12245
|
+
*/
|
|
12246
|
+
resolveDriftLight(
|
|
12247
|
+
this.bakeLightField === null ? null : this.lightField,
|
|
12248
|
+
true,
|
|
12249
|
+
this.frameEye,
|
|
12250
|
+
this.bakeDriftUniforms,
|
|
12251
|
+
);
|
|
12252
|
+
/* And the frame's exact lights, flickering and moving as the frame shades them. */
|
|
12253
|
+
this.bakeLamps.count = resolveBounceLights(env, this.bounceLights);
|
|
12254
|
+
this.bakeLamps.falloff = this.lights.falloff;
|
|
12255
|
+
this.probeBaker.bake(
|
|
12256
|
+
encoder,
|
|
12257
|
+
field,
|
|
12258
|
+
grid,
|
|
12259
|
+
this.probeBaked,
|
|
12260
|
+
this.bakeLightField,
|
|
12261
|
+
this.bakeLamps,
|
|
12262
|
+
{
|
|
12263
|
+
direction: env?.directionalDir ?? NO_SUN,
|
|
12264
|
+
colour: env?.directionalColor ?? NO_SUN,
|
|
12265
|
+
/* What a ray that left the world finds: the sky this frame drew, or its ambient. */
|
|
12266
|
+
ambient: env?.ambient ?? NO_SUN,
|
|
12267
|
+
sky: this.frameSky,
|
|
12268
|
+
},
|
|
12269
|
+
);
|
|
11942
12270
|
/*
|
|
11943
12271
|
* **The bake is what fills the layers now, so it is what marks them filled.** `probeBaked`
|
|
11944
12272
|
* gates the whole array, and a grid nothing rasterised would otherwise never be sampled — the
|
|
@@ -12165,7 +12493,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12165
12493
|
* currently open. Offering it earlier hands a pass a texture of undefined contents, which is
|
|
12166
12494
|
* the failure `probeBaked` was introduced for on the forward path.
|
|
12167
12495
|
*/
|
|
12168
|
-
const usable = this.probeView !== null && this.
|
|
12496
|
+
const usable = this.probeView !== null && this.probeReadable();
|
|
12169
12497
|
if (usable) {
|
|
12170
12498
|
const environment = this.passEnvironment;
|
|
12171
12499
|
environment.view = this.probeView;
|
|
@@ -12653,6 +12981,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12653
12981
|
*/
|
|
12654
12982
|
|
|
12655
12983
|
private bloomScale = 1;
|
|
12984
|
+
/** In scene units, once `setBloom` has moved it; null reads the profile's. */
|
|
12985
|
+
private bloomThresholdSet: number | null = null;
|
|
12656
12986
|
private rushStrength = 0;
|
|
12657
12987
|
private motionBlurScale = 1;
|
|
12658
12988
|
/**
|
|
@@ -12664,6 +12994,51 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12664
12994
|
*/
|
|
12665
12995
|
private readonly veilColor: Vec3 = [0, 0, 0];
|
|
12666
12996
|
private veilAlpha = 0;
|
|
12997
|
+
private vignette = 0;
|
|
12998
|
+
private grain = 0;
|
|
12999
|
+
private grainSeed = 0;
|
|
13000
|
+
private warnedLookWithoutComposite = false;
|
|
13001
|
+
/** Eye adaptation: how far, this frame's time step, and the pass once asked for. */
|
|
13002
|
+
private autoExposure = 0;
|
|
13003
|
+
private autoExposureDt = 0;
|
|
13004
|
+
/** How far each region is brought toward the frame. See `setLocalExposure`. */
|
|
13005
|
+
private localExposure = 0;
|
|
13006
|
+
private exposurePass: ExposurePass | null = null;
|
|
13007
|
+
|
|
13008
|
+
/**
|
|
13009
|
+
* How strongly the lens darkens the frame's corners, held until changed: 0 is none, 0.5 is about
|
|
13010
|
+
* 1.2 stops at the corner. Applied to scene light before the tone curve, as a lens loses light;
|
|
13011
|
+
* see `filmLook.ts`. Needs `screenEffects`, and says so once rather than doing nothing.
|
|
13012
|
+
*/
|
|
13013
|
+
setVignette(strength: number): void {
|
|
13014
|
+
if (!this.hasComposite) {
|
|
13015
|
+
this.warnLookWithoutComposite(strength);
|
|
13016
|
+
return;
|
|
13017
|
+
}
|
|
13018
|
+
this.vignette = clampVignette(strength);
|
|
13019
|
+
}
|
|
13020
|
+
|
|
13021
|
+
/**
|
|
13022
|
+
* Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
|
|
13023
|
+
* grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
|
|
13024
|
+
* its caller: pass a new one each frame for grain that moves, and the same one for a still that
|
|
13025
|
+
* is identical run to run. Applied last, after the grade and the veil; see `filmLook.ts`.
|
|
13026
|
+
*/
|
|
13027
|
+
setFilmGrain(strength: number, seed: number): void {
|
|
13028
|
+
if (!this.hasComposite) {
|
|
13029
|
+
this.warnLookWithoutComposite(strength);
|
|
13030
|
+
return;
|
|
13031
|
+
}
|
|
13032
|
+
this.grain = clampGrain(strength);
|
|
13033
|
+
this.grainSeed = grainSeed(seed);
|
|
13034
|
+
}
|
|
13035
|
+
|
|
13036
|
+
private warnLookWithoutComposite(strength: number): void {
|
|
13037
|
+
if (strength > 0 && !this.warnedLookWithoutComposite) {
|
|
13038
|
+
this.warnedLookWithoutComposite = true;
|
|
13039
|
+
console.warn(FILM_LOOK_WITHOUT_COMPOSITE);
|
|
13040
|
+
}
|
|
13041
|
+
}
|
|
12667
13042
|
|
|
12668
13043
|
/*
|
|
12669
13044
|
* **These three warned that the composite did not exist, and went on warning after it did.**
|
|
@@ -12749,8 +13124,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12749
13124
|
this.gradeSize = lut.size;
|
|
12750
13125
|
}
|
|
12751
13126
|
|
|
12752
|
-
setBloom(scale: number): void {
|
|
13127
|
+
setBloom(scale: number, threshold?: number): void {
|
|
12753
13128
|
this.bloomScale = Math.min(Math.max(scale, 0), 1);
|
|
13129
|
+
if (threshold === undefined) return;
|
|
13130
|
+
const next = bloomThresholdOf(threshold);
|
|
13131
|
+
if (next === this.bloomThresholdSet) return;
|
|
13132
|
+
this.bloomThresholdSet = next;
|
|
13133
|
+
/* The blocks are written once per size, so a moved threshold rewrites them now. */
|
|
13134
|
+
if (this.bloomLevels.length > 0) this.writeBloomBlocks();
|
|
12754
13135
|
}
|
|
12755
13136
|
|
|
12756
13137
|
/**
|
|
@@ -12783,6 +13164,62 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12783
13164
|
setCameraMotionBlur(scale: number): void {
|
|
12784
13165
|
this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
|
|
12785
13166
|
}
|
|
13167
|
+
/**
|
|
13168
|
+
* The camera has cut: this frame is a new shot, not the next moment of the last one.
|
|
13169
|
+
*
|
|
13170
|
+
* **Everything temporal reprojects through the previous frame's view**: the motion blur measures
|
|
13171
|
+
* its smear against it, and the temporal resolve reads its history through it. A cut is a jump
|
|
13172
|
+
* the renderer cannot tell from a fast camera, so without this a transport that seeks, a respawn
|
|
13173
|
+
* or an edit smears its first frame along the whole jump and blends in a picture of somewhere
|
|
13174
|
+
* else. The caller knows it cut; this is how it says so, before the frame's `beginFrame`.
|
|
13175
|
+
*
|
|
13176
|
+
* What it gives up is one frame of history: the frame after a cut has no blur and no temporal
|
|
13177
|
+
* blend, as the first frame of a session has none. Nothing else is reset.
|
|
13178
|
+
*/
|
|
13179
|
+
cameraCut(): void {
|
|
13180
|
+
this.hasPreviousView = false;
|
|
13181
|
+
this.temporalHistory.invalidate();
|
|
13182
|
+
/* The reconstruction keeps histories of its own, and they are the last shot's too. */
|
|
13183
|
+
this.reconHasHistory = false;
|
|
13184
|
+
/* And the eye was adapted to the last shot. */
|
|
13185
|
+
this.exposurePass?.cut();
|
|
13186
|
+
}
|
|
13187
|
+
|
|
13188
|
+
/**
|
|
13189
|
+
* Eye adaptation. **The other backend's `setAutoExposure` carries the reasoning**, whole; this is
|
|
13190
|
+
* the same setter over the same state, and the pass is built the first time it is asked for, so
|
|
13191
|
+
* a renderer that never adapts carries none of it.
|
|
13192
|
+
*/
|
|
13193
|
+
setAutoExposure(strength: number, dtSec: number): void {
|
|
13194
|
+
if (!this.hasComposite || !this.quality.hdrScene) {
|
|
13195
|
+
this.warnLookWithoutComposite(strength);
|
|
13196
|
+
return;
|
|
13197
|
+
}
|
|
13198
|
+
this.autoExposure = clampAutoExposure(strength);
|
|
13199
|
+
this.autoExposureDt = dtSec;
|
|
13200
|
+
if (this.autoExposure > 0) this.ensureExposurePass();
|
|
13201
|
+
}
|
|
13202
|
+
|
|
13203
|
+
/**
|
|
13204
|
+
* Local exposure. **The other backend's `setLocalExposure` carries the reasoning**, whole; this is
|
|
13205
|
+
* the same setter over the same state, sharing eye adaptation's pass, which is built the first
|
|
13206
|
+
* time either is asked for.
|
|
13207
|
+
*/
|
|
13208
|
+
setLocalExposure(strength: number): void {
|
|
13209
|
+
if (!this.hasComposite || !this.quality.hdrScene) {
|
|
13210
|
+
this.warnLookWithoutComposite(strength);
|
|
13211
|
+
return;
|
|
13212
|
+
}
|
|
13213
|
+
this.localExposure = clampLocalExposure(strength);
|
|
13214
|
+
if (this.localExposure > 0) this.ensureExposurePass();
|
|
13215
|
+
}
|
|
13216
|
+
|
|
13217
|
+
/** The exposure pass, built once; the composite's group holds placeholders until it exists. */
|
|
13218
|
+
private ensureExposurePass(): void {
|
|
13219
|
+
if (this.exposurePass !== null) return;
|
|
13220
|
+
this.exposurePass = new ExposurePass(this.surface.device, this.pipelines, this.postSampler);
|
|
13221
|
+
this.rebuildRushBindGroup();
|
|
13222
|
+
}
|
|
12786
13223
|
|
|
12787
13224
|
/**
|
|
12788
13225
|
* Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
|
|
@@ -13022,6 +13459,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13022
13459
|
* over a lobe-convolved one.
|
|
13023
13460
|
*/
|
|
13024
13461
|
private readonly probe: GPUTexture | null;
|
|
13462
|
+
/** The views, groups and attachments a bake draws with, made once. See `probeTargets.ts`. */
|
|
13463
|
+
private readonly probeTargets: ProbeTargets | null;
|
|
13025
13464
|
/**
|
|
13026
13465
|
* The prefiltered cube: what the lit pass samples, one GGX-convolved level per roughness.
|
|
13027
13466
|
*
|
|
@@ -13053,6 +13492,19 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13053
13492
|
private readonly probeMipLayout: GPUBindGroupLayout;
|
|
13054
13493
|
/** The photometric atlas, allocated on first use. See `setIesProfiles`. */
|
|
13055
13494
|
private iesTexture: GPUTexture | null = null;
|
|
13495
|
+
/** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
|
|
13496
|
+
private lightField: LightField | null = null;
|
|
13497
|
+
private driftIndex: GPUTexture | null = null;
|
|
13498
|
+
private driftAtlas: GPUTexture | null = null;
|
|
13499
|
+
private driftIndexView: GPUTextureView | null = null;
|
|
13500
|
+
private driftAtlasView: GPUTextureView | null = null;
|
|
13501
|
+
private readonly driftUniforms = createDriftLightUniforms();
|
|
13502
|
+
/** The same field resolved for a traced probe, and what the baker binds; null while not whole. */
|
|
13503
|
+
private readonly bakeDriftUniforms = createDriftLightUniforms();
|
|
13504
|
+
private bakeLightField: ProbeBakeLightField | null = null;
|
|
13505
|
+
/** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
|
|
13506
|
+
private readonly bounceLights = createBounceLights();
|
|
13507
|
+
private readonly bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
|
|
13056
13508
|
private iesView: GPUTextureView | null = null;
|
|
13057
13509
|
private cookieTexture: GPUTexture | null = null;
|
|
13058
13510
|
private cookieView: GPUTextureView | null = null;
|
|
@@ -13077,8 +13529,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13077
13529
|
* mirroring uninitialised memory is worse than a car mirroring a gradient — the rule one probe
|
|
13078
13530
|
* already had, unchanged by there being more of them.
|
|
13079
13531
|
*/
|
|
13080
|
-
private
|
|
13081
|
-
|
|
13532
|
+
private probeSweeps: ProbeSweeps | null = null;
|
|
13533
|
+
/** `uProbeGridSets`, rewritten in place. */
|
|
13534
|
+
private readonly probeSets = new Float32Array(3);
|
|
13082
13535
|
/**
|
|
13083
13536
|
* Whether a bake is running, which is when the cube may be neither sampled nor bound.
|
|
13084
13537
|
*
|
|
@@ -13086,15 +13539,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13086
13539
|
* load-bearing rather than defensive: WebGPU rejects a pass that holds the texture it writes.
|
|
13087
13540
|
*/
|
|
13088
13541
|
private probePassActive = false;
|
|
13542
|
+
/** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
|
|
13543
|
+
private probeBounce = false;
|
|
13544
|
+
|
|
13545
|
+
/**
|
|
13546
|
+
* Whether the shading may read the grid now: once every probe is filled, and not during a bake
|
|
13547
|
+
* unless it is a bounce bake. A bounce bake's faces are drawn into the probe's own cube and only
|
|
13548
|
+
* the resolve writes the array, in a pass of its own, so no pass both samples and writes it.
|
|
13549
|
+
*/
|
|
13550
|
+
private probeReadable(): boolean {
|
|
13551
|
+
return this.probeBaked && (!this.probePassActive || this.probeBounce);
|
|
13552
|
+
}
|
|
13089
13553
|
|
|
13090
13554
|
/** How many mip levels the roughness blur has to work with. `flat.ts` reads this. */
|
|
13091
13555
|
private get probeMaxLod(): number {
|
|
13092
13556
|
return ggxMaxLevelFor(this.probeEdge);
|
|
13093
13557
|
}
|
|
13094
13558
|
|
|
13095
|
-
/** Whether
|
|
13559
|
+
/** Whether a whole sweep of the grid has been convolved. See `probeSweeps.ts`. */
|
|
13096
13560
|
private get probeBaked(): boolean {
|
|
13097
|
-
return this.probes !== null && this.
|
|
13561
|
+
return this.probes !== null && this.probeSweeps?.ready === true;
|
|
13098
13562
|
}
|
|
13099
13563
|
|
|
13100
13564
|
/**
|
|
@@ -13114,11 +13578,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13114
13578
|
* mistake, and `setProbeGrid` says it about the array being replaced.
|
|
13115
13579
|
*/
|
|
13116
13580
|
private markProbeFilled(layer: number): void {
|
|
13117
|
-
|
|
13118
|
-
|
|
13119
|
-
this.
|
|
13120
|
-
this.probeFilledCount++;
|
|
13121
|
-
if (this.probeBaked) this.rebuildFlatGroupsForNewRings();
|
|
13581
|
+
const was = this.probeBaked;
|
|
13582
|
+
this.probeSweeps?.baked(layer);
|
|
13583
|
+
if (!was && this.probeBaked) this.rebuildFlatGroupsForNewRings();
|
|
13122
13584
|
}
|
|
13123
13585
|
|
|
13124
13586
|
/**
|
|
@@ -13129,6 +13591,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13129
13591
|
*/
|
|
13130
13592
|
private allocateProbeArray(layers: number, device: GPUDevice): void {
|
|
13131
13593
|
this.probeArray?.destroy();
|
|
13594
|
+
this.probeTargets?.forgetArray();
|
|
13132
13595
|
this.probeArray = device.createTexture({
|
|
13133
13596
|
label: 'probe.array',
|
|
13134
13597
|
size: [this.probeEdge, this.probeEdge, layers],
|
|
@@ -13138,8 +13601,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13138
13601
|
usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
|
|
13139
13602
|
});
|
|
13140
13603
|
this.probeView = this.probeArray.createView({ dimension: '2d-array' });
|
|
13141
|
-
this.probeFilled = new Uint8Array(layers);
|
|
13142
|
-
this.probeFilledCount = 0;
|
|
13143
13604
|
|
|
13144
13605
|
/*
|
|
13145
13606
|
* **Cleared here, because a texture no pass has written holds undefined contents** — the same
|
|
@@ -13196,8 +13657,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13196
13657
|
const current = this.probes;
|
|
13197
13658
|
if (current !== null && this.probeArray !== null && sameGrid(current, grid)) return true;
|
|
13198
13659
|
this.probes = grid;
|
|
13199
|
-
|
|
13200
|
-
|
|
13660
|
+
/* A traced grid writes its layers itself, so it keeps one set. See `ProbeGridOptions.crossfade`. */
|
|
13661
|
+
const crossfade = grid.crossfade && !this.quality.indirectLight;
|
|
13662
|
+
const sets = crossfade ? 3 : 1;
|
|
13663
|
+
if (
|
|
13664
|
+
this.probeArray === null ||
|
|
13665
|
+
this.probeArray.depthOrArrayLayers !== grid.layers * sets ||
|
|
13666
|
+
this.probeSweeps?.sets !== sets
|
|
13667
|
+
) {
|
|
13668
|
+
this.probeSweeps = new ProbeSweeps(grid.layers, crossfade);
|
|
13669
|
+
this.allocateProbeArray(grid.layers * sets, this.surface.device);
|
|
13201
13670
|
this.rebuildFlatGroupsForNewRings();
|
|
13202
13671
|
}
|
|
13203
13672
|
return true;
|
|
@@ -13273,40 +13742,20 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13273
13742
|
camera.roll = 0;
|
|
13274
13743
|
|
|
13275
13744
|
const { device } = this.surface;
|
|
13276
|
-
const
|
|
13277
|
-
|
|
13278
|
-
size: [this.probeSize, this.probeSize],
|
|
13279
|
-
format: DEPTH_FORMAT,
|
|
13280
|
-
/*
|
|
13281
|
-
* **The world's sample count, because a bake draws the world with the world's pipelines.**
|
|
13282
|
-
* A pipeline whose count disagrees with its attachment is rejected at `finish` and takes
|
|
13283
|
-
* the whole bake with it, and WebGPU requires every attachment in a pass to agree — so a
|
|
13284
|
-
* single-sample depth beside a four-sample colour is refused in words that read as a depth
|
|
13285
|
-
* problem and are a multisampling one.
|
|
13286
|
-
*/
|
|
13287
|
-
sampleCount: this.samples,
|
|
13288
|
-
usage: 0x10, // RENDER_ATTACHMENT
|
|
13289
|
-
});
|
|
13290
|
-
const depthView = depth.createView();
|
|
13745
|
+
const targets = this.probeTargets;
|
|
13746
|
+
if (targets === null) return false;
|
|
13291
13747
|
/*
|
|
13292
|
-
* The
|
|
13293
|
-
*
|
|
13294
|
-
*
|
|
13295
|
-
*
|
|
13296
|
-
*
|
|
13297
|
-
*
|
|
13748
|
+
* The depth, at the world's sample count because a bake draws the world with the world's
|
|
13749
|
+
* pipelines: a pipeline whose count disagrees with its attachment is rejected at `finish` and
|
|
13750
|
+
* takes the whole bake with it, in words that read as a depth problem and are a multisampling
|
|
13751
|
+
* one. And the multisampled twin the faces are drawn into and resolved from, since **a cube
|
|
13752
|
+
* face cannot be both multisampled and sampled** — the same bind the mirror is in. Both kept
|
|
13753
|
+
* between bakes: see `probeTargets.ts`.
|
|
13298
13754
|
*/
|
|
13299
|
-
const colorMsaa =
|
|
13300
|
-
this.samples
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
size: [this.probeSize, this.probeSize],
|
|
13304
|
-
format: this.pipelines.format,
|
|
13305
|
-
sampleCount: this.samples,
|
|
13306
|
-
usage: 0x10, // RENDER_ATTACHMENT
|
|
13307
|
-
})
|
|
13308
|
-
: null;
|
|
13309
|
-
const colorMsaaView = colorMsaa?.createView() ?? null;
|
|
13755
|
+
const { depth: depthView, colorMsaa: colorMsaaView } = targets.attachments(
|
|
13756
|
+
this.samples,
|
|
13757
|
+
this.pipelines.format,
|
|
13758
|
+
);
|
|
13310
13759
|
|
|
13311
13760
|
/*
|
|
13312
13761
|
* The frame's own pass is set aside and restored, because `drawFace` calls straight back into
|
|
@@ -13325,16 +13774,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13325
13774
|
*/
|
|
13326
13775
|
if (this.quality.frameGraph) this.flushGraph();
|
|
13327
13776
|
const savedEncoder = this.encoder;
|
|
13777
|
+
/*
|
|
13778
|
+
* **The rings are marked here and given back after the submit below**, because this bake is its
|
|
13779
|
+
* own encoder: once it is submitted nothing in the frame can read its slots, and a grid baked in
|
|
13780
|
+
* one call would otherwise spend the frame's whole ring on probes and skip the draws past it.
|
|
13781
|
+
*/
|
|
13782
|
+
this.markRings();
|
|
13783
|
+
const skinMark = this.skinPalettes.mark();
|
|
13784
|
+
const frameEpoch = this.encoderEpoch;
|
|
13785
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
13328
13786
|
const encoder = device.createCommandEncoder({ label: 'probe' });
|
|
13329
13787
|
this.encoder = encoder;
|
|
13330
13788
|
/*
|
|
13331
|
-
* Fenced before the first face and released after the last,
|
|
13332
|
-
*
|
|
13333
|
-
*
|
|
13334
|
-
*
|
|
13789
|
+
* Fenced before the first face and released after the last, so the grid is not bound while a
|
|
13790
|
+
* plain bake is drawn. **Rebuilt only where that changes what the group binds**: a bounce bake
|
|
13791
|
+
* of a whole grid binds it exactly as the frame does, and it is the bake a scene repeats every
|
|
13792
|
+
* frame, a few faces at a time. See `rebindEnvironment`.
|
|
13335
13793
|
*/
|
|
13794
|
+
this.probeBounce = options?.bounce ?? false;
|
|
13336
13795
|
this.probePassActive = true;
|
|
13337
|
-
this.
|
|
13796
|
+
this.rebindEnvironment();
|
|
13338
13797
|
/*
|
|
13339
13798
|
* **A probe stores radiance, not display pixels.** See `renderer.ts` for the whole argument:
|
|
13340
13799
|
* baking through the output transform crushes every highlight with the tone curve before it
|
|
@@ -13347,7 +13806,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13347
13806
|
this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
|
|
13348
13807
|
this.materials.dirty();
|
|
13349
13808
|
|
|
13350
|
-
|
|
13809
|
+
/* Only the faces asked for; the chain waits for the sixth. See `ProbeBakeOptions.faces`. */
|
|
13810
|
+
const [firstFace, endFace] = probeFaceRange(options);
|
|
13811
|
+
for (let face = firstFace; face < endFace; face++) {
|
|
13351
13812
|
const aim = PROBE_FACES[face];
|
|
13352
13813
|
if (aim === undefined) continue;
|
|
13353
13814
|
camera.yaw = aim.yaw;
|
|
@@ -13358,13 +13819,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13358
13819
|
*/
|
|
13359
13820
|
camera.updateMatrices(1);
|
|
13360
13821
|
|
|
13361
|
-
const faceView =
|
|
13362
|
-
dimension: '2d',
|
|
13363
|
-
baseArrayLayer: face,
|
|
13364
|
-
arrayLayerCount: 1,
|
|
13365
|
-
baseMipLevel: 0,
|
|
13366
|
-
mipLevelCount: 1,
|
|
13367
|
-
});
|
|
13822
|
+
const faceView = targets.faceViews[face] as GPUTextureView;
|
|
13368
13823
|
this.pass = encoder.beginRenderPass({
|
|
13369
13824
|
label: `probe.face${face}`,
|
|
13370
13825
|
colorAttachments: [
|
|
@@ -13413,20 +13868,22 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13413
13868
|
this.pass.end();
|
|
13414
13869
|
}
|
|
13415
13870
|
this.pass = null;
|
|
13416
|
-
|
|
13871
|
+
const whole = endFace === PROBE_FACES.length;
|
|
13872
|
+
if (whole) this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
|
|
13417
13873
|
this.flushRings();
|
|
13418
13874
|
device.queue.submit([encoder.finish()]);
|
|
13419
|
-
|
|
13420
|
-
|
|
13875
|
+
this.rewindRings(skinMark);
|
|
13876
|
+
this.encoderEpoch = frameEpoch;
|
|
13421
13877
|
|
|
13422
13878
|
this.encoder = savedEncoder;
|
|
13423
13879
|
this.pass = savedPass;
|
|
13424
|
-
this.markProbeFilled(layer);
|
|
13880
|
+
if (whole) this.markProbeFilled(layer);
|
|
13425
13881
|
this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
|
|
13426
13882
|
this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
|
|
13427
13883
|
this.materials.dirty();
|
|
13428
13884
|
this.probePassActive = false;
|
|
13429
|
-
this.
|
|
13885
|
+
this.probeBounce = false;
|
|
13886
|
+
this.rebindEnvironment();
|
|
13430
13887
|
return true;
|
|
13431
13888
|
}
|
|
13432
13889
|
|
|
@@ -13577,6 +14034,92 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13577
14034
|
this.rebuildFlatBindGroup();
|
|
13578
14035
|
}
|
|
13579
14036
|
|
|
14037
|
+
/**
|
|
14038
|
+
* Sum a scene's many fixed lights into a DriftLight field. The same contract as
|
|
14039
|
+
* `Renderer.createLightField`; see `driftLight/lightField.ts`.
|
|
14040
|
+
*/
|
|
14041
|
+
createLightField(
|
|
14042
|
+
lights: readonly LightFieldSource[],
|
|
14043
|
+
options: Omit<LightFieldOptions, 'falloff'> = {},
|
|
14044
|
+
): LightField {
|
|
14045
|
+
this.forgetLightFieldVolumes();
|
|
14046
|
+
const field = new LightField(lights, { ...options, falloff: this.quality.pointLightFalloff });
|
|
14047
|
+
this.lightField = field;
|
|
14048
|
+
return field;
|
|
14049
|
+
}
|
|
14050
|
+
|
|
14051
|
+
/** Let go of the field and its volumes; the scene's lights are all exact again. */
|
|
14052
|
+
disposeLightField(): void {
|
|
14053
|
+
this.forgetLightFieldVolumes();
|
|
14054
|
+
this.lightField = null;
|
|
14055
|
+
}
|
|
14056
|
+
|
|
14057
|
+
private forgetLightFieldVolumes(): void {
|
|
14058
|
+
const bound = this.driftAtlas !== null;
|
|
14059
|
+
this.driftIndex?.destroy();
|
|
14060
|
+
this.driftAtlas?.destroy();
|
|
14061
|
+
this.driftIndex = null;
|
|
14062
|
+
this.driftAtlas = null;
|
|
14063
|
+
this.driftIndexView = null;
|
|
14064
|
+
this.driftAtlasView = null;
|
|
14065
|
+
this.bakeLightField = null;
|
|
14066
|
+
/* The groups hold the destroyed volumes, and a group holding a destroyed texture fails. */
|
|
14067
|
+
if (bound && !this.surface.lost) this.rebuildFlatBindGroup();
|
|
14068
|
+
}
|
|
14069
|
+
|
|
14070
|
+
/**
|
|
14071
|
+
* Upload the field's two volumes, once, when its last brick has baked, and rebuild the groups
|
|
14072
|
+
* that bind them: nothing samples them before that, so uploading as bricks land would be work
|
|
14073
|
+
* for no picture, and a rebuild per brick would be a bind group per brick.
|
|
14074
|
+
*/
|
|
14075
|
+
private uploadLightField(): void {
|
|
14076
|
+
const field = this.lightField;
|
|
14077
|
+
if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
|
|
14078
|
+
return;
|
|
14079
|
+
}
|
|
14080
|
+
if (this.surface.lost) return;
|
|
14081
|
+
const { device } = this.surface;
|
|
14082
|
+
const [ix, iy, iz] = field.layout.dims;
|
|
14083
|
+
const index = device.createTexture({
|
|
14084
|
+
label: 'driftLight.index',
|
|
14085
|
+
size: [ix, iy, iz],
|
|
14086
|
+
dimension: '3d',
|
|
14087
|
+
format: 'r32uint',
|
|
14088
|
+
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
14089
|
+
});
|
|
14090
|
+
device.queue.writeTexture(
|
|
14091
|
+
{ texture: index },
|
|
14092
|
+
field.layout.index,
|
|
14093
|
+
{ bytesPerRow: ix * 4, rowsPerImage: iy },
|
|
14094
|
+
[ix, iy, iz],
|
|
14095
|
+
);
|
|
14096
|
+
const [aw, ah, ad] = field.atlasSize;
|
|
14097
|
+
const atlas = device.createTexture({
|
|
14098
|
+
label: 'driftLight.atlas',
|
|
14099
|
+
size: [aw, ah, ad],
|
|
14100
|
+
dimension: '3d',
|
|
14101
|
+
format: 'rgba16float',
|
|
14102
|
+
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
14103
|
+
});
|
|
14104
|
+
device.queue.writeTexture(
|
|
14105
|
+
{ texture: atlas },
|
|
14106
|
+
field.atlas,
|
|
14107
|
+
{ bytesPerRow: aw * 8, rowsPerImage: ah },
|
|
14108
|
+
[aw, ah, ad],
|
|
14109
|
+
);
|
|
14110
|
+
this.driftIndex = index;
|
|
14111
|
+
this.driftAtlas = atlas;
|
|
14112
|
+
this.driftIndexView = index.createView({ dimension: '3d' });
|
|
14113
|
+
this.driftAtlasView = atlas.createView({ dimension: '3d' });
|
|
14114
|
+
this.bakeLightField = {
|
|
14115
|
+
index: this.driftIndexView,
|
|
14116
|
+
atlas: this.driftAtlasView,
|
|
14117
|
+
light: this.bakeDriftUniforms.light,
|
|
14118
|
+
origin: this.bakeDriftUniforms.origin,
|
|
14119
|
+
};
|
|
14120
|
+
this.rebuildFlatBindGroup();
|
|
14121
|
+
}
|
|
14122
|
+
|
|
13580
14123
|
setIesProfiles(profiles: readonly PhotometricProfile[]): void {
|
|
13581
14124
|
if (this.surface.lost) return;
|
|
13582
14125
|
const { device } = this.surface;
|
|
@@ -13624,6 +14167,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13624
14167
|
/** Each level from the one above it, six faces at a time. See `probePass.ts`. */
|
|
13625
14168
|
private buildProbeChain(encoder: GPUCommandEncoder, probe: GPUTexture, layer: number): void {
|
|
13626
14169
|
const { device } = this.surface;
|
|
14170
|
+
const targets = this.probeTargets;
|
|
14171
|
+
if (targets === null) return;
|
|
13627
14172
|
/*
|
|
13628
14173
|
* **Reset and flushed here, because a bake is not a frame.**
|
|
13629
14174
|
*
|
|
@@ -13652,12 +14197,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13652
14197
|
);
|
|
13653
14198
|
const levels = probeLevels(this.probeSize);
|
|
13654
14199
|
for (let level = 1; level < levels; level++) {
|
|
13655
|
-
/*
|
|
13656
|
-
const
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
|
|
14200
|
+
/* The whole cube at the level above, so the blur can cross a face edge, in a group kept. */
|
|
14201
|
+
const group = targets.chainGroup(
|
|
14202
|
+
this.probeMipLayout,
|
|
14203
|
+
this.probeMipUniforms.buffer,
|
|
14204
|
+
this.probeSampler,
|
|
14205
|
+
level,
|
|
14206
|
+
);
|
|
13661
14207
|
for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
|
|
13662
14208
|
const basis = PROBE_FACE_BASIS[face];
|
|
13663
14209
|
if (basis === undefined) continue;
|
|
@@ -13674,22 +14220,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13674
14220
|
this.probeMipUniforms.flush();
|
|
13675
14221
|
return;
|
|
13676
14222
|
}
|
|
13677
|
-
this.probeMipUniforms.writeFloats(slot, 0,
|
|
13678
|
-
this.probeMipUniforms.writeFloats(slot, 16,
|
|
13679
|
-
this.probeMipUniforms.writeFloats(slot, 32,
|
|
13680
|
-
this.probeMipUniforms.writeFloats(slot, 48,
|
|
14223
|
+
this.probeMipUniforms.writeFloats(slot, 0, padded(basis.basisX));
|
|
14224
|
+
this.probeMipUniforms.writeFloats(slot, 16, padded(basis.basisY));
|
|
14225
|
+
this.probeMipUniforms.writeFloats(slot, 32, padded(basis.forward));
|
|
14226
|
+
this.probeMipUniforms.writeFloats(slot, 48, ZERO4);
|
|
13681
14227
|
|
|
13682
14228
|
const pass = encoder.beginRenderPass({
|
|
13683
14229
|
label: `probe.mip${level}.face${face}`,
|
|
13684
14230
|
colorAttachments: [
|
|
13685
14231
|
{
|
|
13686
|
-
view:
|
|
13687
|
-
dimension: '2d',
|
|
13688
|
-
baseArrayLayer: face,
|
|
13689
|
-
arrayLayerCount: 1,
|
|
13690
|
-
baseMipLevel: level,
|
|
13691
|
-
mipLevelCount: 1,
|
|
13692
|
-
}),
|
|
14232
|
+
view: targets.chainTarget(level, face),
|
|
13693
14233
|
loadOp: 'clear',
|
|
13694
14234
|
storeOp: 'store',
|
|
13695
14235
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -13697,21 +14237,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13697
14237
|
],
|
|
13698
14238
|
});
|
|
13699
14239
|
pass.setPipeline(pipeline);
|
|
13700
|
-
pass.setBindGroup(
|
|
13701
|
-
0,
|
|
13702
|
-
device.createBindGroup({
|
|
13703
|
-
layout: this.probeMipLayout,
|
|
13704
|
-
entries: [
|
|
13705
|
-
{
|
|
13706
|
-
binding: 0,
|
|
13707
|
-
resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
|
|
13708
|
-
},
|
|
13709
|
-
{ binding: 1, resource: source },
|
|
13710
|
-
{ binding: 2, resource: this.probeSampler },
|
|
13711
|
-
],
|
|
13712
|
-
}),
|
|
13713
|
-
[slot],
|
|
13714
|
-
);
|
|
14240
|
+
pass.setBindGroup(0, group, [slot]);
|
|
13715
14241
|
pass.draw(3);
|
|
13716
14242
|
pass.end();
|
|
13717
14243
|
}
|
|
@@ -13737,8 +14263,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13737
14263
|
this.probePrefilterLayout,
|
|
13738
14264
|
this.pipelines.format,
|
|
13739
14265
|
);
|
|
13740
|
-
/* The whole chain, so a wide sample can select a level from it. */
|
|
13741
|
-
const
|
|
14266
|
+
/* The whole chain, so a wide sample can select a level from it, in a group kept. */
|
|
14267
|
+
const group = targets.prefilterGroupFor(
|
|
14268
|
+
this.probePrefilterLayout,
|
|
14269
|
+
this.probeMipUniforms.buffer,
|
|
14270
|
+
this.probeSampler,
|
|
14271
|
+
);
|
|
13742
14272
|
const offsets = PREFILTER_UNIFORM_OFFSETS;
|
|
13743
14273
|
const topLevel = levels - 1;
|
|
13744
14274
|
const samples = this.quality.environmentPrefilterSamples;
|
|
@@ -13746,13 +14276,20 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13746
14276
|
const irradianceLevel = irradianceLevelFor(edge);
|
|
13747
14277
|
const ggxTop = ggxMaxLevelFor(edge);
|
|
13748
14278
|
const boxChain = !this.quality.environmentPrefilter;
|
|
14279
|
+
/*
|
|
14280
|
+
* **The diffuse level of a layer the trace has written is the trace's**, so a rasterised bake
|
|
14281
|
+
* stops one level short of it. See `ProbeBaker.traced`: two writers of one level was light
|
|
14282
|
+
* sweeping down a courtyard a probe at a time with the clock paused.
|
|
14283
|
+
*/
|
|
14284
|
+
const lastLevel =
|
|
14285
|
+
this.probeBaker?.traced(layer) === true ? irradianceLevel - 1 : irradianceLevel;
|
|
13749
14286
|
|
|
13750
14287
|
/*
|
|
13751
14288
|
* **One draw a level, where the cube needed six.** An octahedral map has no faces, so the
|
|
13752
14289
|
* six-way loop is gone rather than moved, and a probe's convolution is six times fewer
|
|
13753
14290
|
* passes than the cube it replaces.
|
|
13754
14291
|
*/
|
|
13755
|
-
for (let level = 0; level <=
|
|
14292
|
+
for (let level = 0; level <= lastLevel; level++) {
|
|
13756
14293
|
const irradiance = level === irradianceLevel;
|
|
13757
14294
|
const roughness = irradiance ? 1 : roughnessForLevel(level, ggxTop);
|
|
13758
14295
|
const slot = this.probeMipUniforms.allocate();
|
|
@@ -13771,32 +14308,40 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13771
14308
|
/* Written at the generator's own offsets rather than at literals, because a roughness at
|
|
13772
14309
|
the wrong offset is a level convolved for some other roughness — which looks like a
|
|
13773
14310
|
plausible reflection and points at nothing. */
|
|
13774
|
-
this.probeMipUniforms.writeFloats(
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
this.probeMipUniforms.writeFloats(
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
14311
|
+
this.probeMipUniforms.writeFloats(
|
|
14312
|
+
slot,
|
|
14313
|
+
offsets.uPrefilterEdge.offset,
|
|
14314
|
+
one(Math.max(1, edge >> level)),
|
|
14315
|
+
);
|
|
14316
|
+
this.probeMipUniforms.writeFloats(
|
|
14317
|
+
slot,
|
|
14318
|
+
offsets.uPrefilterIrradiance.offset,
|
|
14319
|
+
one(irradiance ? 1 : 0),
|
|
14320
|
+
);
|
|
14321
|
+
this.probeMipUniforms.writeFloats(
|
|
14322
|
+
slot,
|
|
14323
|
+
offsets.uPrefilterBox.offset,
|
|
14324
|
+
one(boxChain ? 1 : 0),
|
|
14325
|
+
);
|
|
14326
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterLevel.offset, one(level));
|
|
14327
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterRoughness.offset, one(roughness));
|
|
14328
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSamples.offset, one(samples));
|
|
14329
|
+
this.probeMipUniforms.writeFloats(
|
|
14330
|
+
slot,
|
|
14331
|
+
offsets.uPrefilterSourceTexels.offset,
|
|
14332
|
+
one(this.probeSize),
|
|
14333
|
+
);
|
|
14334
|
+
this.probeMipUniforms.writeFloats(
|
|
14335
|
+
slot,
|
|
14336
|
+
offsets.uPrefilterSourceMaxLod.offset,
|
|
14337
|
+
one(topLevel),
|
|
14338
|
+
);
|
|
13788
14339
|
|
|
13789
14340
|
const pass = encoder.beginRenderPass({
|
|
13790
14341
|
label: `probe.prefilter${level}.layer${layer}`,
|
|
13791
14342
|
colorAttachments: [
|
|
13792
14343
|
{
|
|
13793
|
-
view:
|
|
13794
|
-
dimension: '2d',
|
|
13795
|
-
baseArrayLayer: layer,
|
|
13796
|
-
arrayLayerCount: 1,
|
|
13797
|
-
baseMipLevel: level,
|
|
13798
|
-
mipLevelCount: 1,
|
|
13799
|
-
}),
|
|
14344
|
+
view: targets.arrayView(prefilterTarget, layer, level),
|
|
13800
14345
|
loadOp: 'clear',
|
|
13801
14346
|
storeOp: 'store',
|
|
13802
14347
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -13804,21 +14349,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13804
14349
|
],
|
|
13805
14350
|
});
|
|
13806
14351
|
pass.setPipeline(prefilter);
|
|
13807
|
-
pass.setBindGroup(
|
|
13808
|
-
0,
|
|
13809
|
-
device.createBindGroup({
|
|
13810
|
-
layout: this.probePrefilterLayout,
|
|
13811
|
-
entries: [
|
|
13812
|
-
{
|
|
13813
|
-
binding: PREFILTER_UNIFORM_BINDING,
|
|
13814
|
-
resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
|
|
13815
|
-
},
|
|
13816
|
-
{ binding: PREFILTER_TEXTURE_BINDING, resource: source },
|
|
13817
|
-
{ binding: PREFILTER_SAMPLER_BINDING, resource: this.probeSampler },
|
|
13818
|
-
],
|
|
13819
|
-
}),
|
|
13820
|
-
[slot],
|
|
13821
|
-
);
|
|
14352
|
+
pass.setBindGroup(0, group, [slot]);
|
|
13822
14353
|
pass.draw(3);
|
|
13823
14354
|
pass.end();
|
|
13824
14355
|
}
|
|
@@ -14297,7 +14828,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14297
14828
|
f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.frameHeight);
|
|
14298
14829
|
/* In scene units, which is why an HDR target is what makes a threshold above 1 mean
|
|
14299
14830
|
anything: against a clamped buffer nothing is ever brighter than white. */
|
|
14300
|
-
f[slot(0) + prefilter('uThreshold')] = this.quality.bloomThreshold;
|
|
14831
|
+
f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
|
|
14301
14832
|
|
|
14302
14833
|
/* Each downsample's texel is its *source's*, which is the level above it. */
|
|
14303
14834
|
const down = (name: string): number => this.postField(BLOOM_DOWNSAMPLE_FIELDS, name);
|
|
@@ -14415,6 +14946,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14415
14946
|
|
|
14416
14947
|
this.fieldComposer?.dispose();
|
|
14417
14948
|
this.fieldComposer = null;
|
|
14949
|
+
this.driftIndex?.destroy();
|
|
14950
|
+
this.driftAtlas?.destroy();
|
|
14418
14951
|
|
|
14419
14952
|
this.pass = null;
|
|
14420
14953
|
this.encoder = null;
|
|
@@ -14422,6 +14955,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14422
14955
|
this.gpuTimer.dispose();
|
|
14423
14956
|
this.reflection?.dispose();
|
|
14424
14957
|
this.reflection = null;
|
|
14958
|
+
this.probeTargets?.dispose();
|
|
14959
|
+
this.exposurePass?.dispose();
|
|
14960
|
+
this.exposurePass = null;
|
|
14425
14961
|
this.surface.dispose();
|
|
14426
14962
|
}
|
|
14427
14963
|
}
|