@driftengine/core 4.3.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +282 -0
- package/README.md +1 -1
- package/dist/cinematic/cameraPath.d.ts +53 -0
- package/dist/cinematic/cameraPath.js +118 -0
- package/dist/environment/daylight.d.ts +77 -0
- package/dist/environment/daylight.js +77 -0
- package/dist/geometry/meshConcat.d.ts +9 -0
- package/dist/geometry/meshConcat.js +101 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/dist/render/ambientOcclusionPass.js +5 -1
- package/dist/render/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +147 -1
- package/dist/render/backend/webgl2/renderer.js +402 -23
- package/dist/render/backend/webgpu/buffers.js +46 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
- package/dist/render/backend/webgpu/depthPass.js +75 -14
- package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
- package/dist/render/backend/webgpu/exposurePass.js +190 -0
- package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
- package/dist/render/backend/webgpu/fieldCompose.js +13 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
- package/dist/render/backend/webgpu/flatPass.js +12 -5
- package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
- package/dist/render/backend/webgpu/particlePass.js +1 -0
- package/dist/render/backend/webgpu/postPass.d.ts +13 -3
- package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
- package/dist/render/backend/webgpu/probeBake.js +97 -7
- package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
- package/dist/render/backend/webgpu/probeTargets.js +180 -0
- package/dist/render/backend/webgpu/renderer.d.ts +174 -11
- package/dist/render/backend/webgpu/renderer.js +682 -252
- package/dist/render/backend/webgpu/scatterPass.js +7 -7
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
- package/dist/render/backend/webgpu/skyPass.js +5 -1
- package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
- package/dist/render/backend/webgpu/uniformRing.js +53 -3
- package/dist/render/bloomChain.d.ts +6 -0
- package/dist/render/bloomChain.js +8 -0
- package/dist/render/clusteredLights.d.ts +19 -11
- package/dist/render/clusteredLights.js +96 -45
- package/dist/render/cutoutCaster.d.ts +23 -0
- package/dist/render/cutoutCaster.js +20 -0
- package/dist/render/driftLight/bake.d.ts +34 -0
- package/dist/render/driftLight/bake.js +95 -0
- package/dist/render/driftLight/falloff.d.ts +16 -0
- package/dist/render/driftLight/falloff.js +23 -0
- package/dist/render/driftLight/layout.d.ts +59 -0
- package/dist/render/driftLight/layout.js +138 -0
- package/dist/render/driftLight/lightField.d.ts +108 -0
- package/dist/render/driftLight/lightField.js +179 -0
- package/dist/render/driftLight/uniforms.d.ts +28 -0
- package/dist/render/driftLight/uniforms.js +41 -0
- package/dist/render/driftLight/visibility.d.ts +18 -0
- package/dist/render/driftLight/visibility.js +47 -0
- package/dist/render/emptyTexture.d.ts +6 -0
- package/dist/render/emptyTexture.js +19 -0
- package/dist/render/exposurePass.d.ts +48 -0
- package/dist/render/exposurePass.js +172 -0
- package/dist/render/flameLight.d.ts +71 -0
- package/dist/render/flameLight.js +63 -0
- package/dist/render/gi/bounceLights.d.ts +34 -0
- package/dist/render/gi/bounceLights.js +66 -0
- package/dist/render/gi/fieldReach.d.ts +12 -0
- package/dist/render/gi/fieldReach.js +62 -0
- package/dist/render/gi/globalField.d.ts +8 -0
- package/dist/render/gi/globalField.js +31 -0
- package/dist/render/gi/probeTrace.d.ts +3 -1
- package/dist/render/gi/probeTrace.js +3 -1
- package/dist/render/gpuCapability.js +19 -4
- package/dist/render/lightBudget.d.ts +11 -0
- package/dist/render/lightBudget.js +11 -0
- package/dist/render/pointLightSelection.d.ts +16 -0
- package/dist/render/pointLightSelection.js +178 -83
- package/dist/render/pointShadowBudget.d.ts +12 -2
- package/dist/render/pointShadowBudget.js +16 -10
- package/dist/render/probeGrid.d.ts +11 -1
- package/dist/render/probeGrid.js +5 -2
- package/dist/render/probeSweeps.d.ts +45 -0
- package/dist/render/probeSweeps.js +77 -0
- package/dist/render/reflectionProbe.d.ts +43 -1
- package/dist/render/reflectionProbe.js +17 -3
- package/dist/render/renderQuality.d.ts +3 -1
- package/dist/render/rendererHarness.d.ts +9 -0
- package/dist/render/rendererHarness.js +18 -3
- package/dist/render/sceneTarget.d.ts +20 -0
- package/dist/render/sceneTarget.js +62 -1
- package/dist/render/shaders/ambientOcclusion.d.ts +63 -6
- package/dist/render/shaders/ambientOcclusion.js +188 -71
- package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
- package/dist/render/shaders/cutoutCoverage.js +40 -0
- package/dist/render/shaders/depth.d.ts +25 -0
- package/dist/render/shaders/depth.js +69 -3
- package/dist/render/shaders/exposure.d.ts +52 -0
- package/dist/render/shaders/exposure.js +124 -0
- package/dist/render/shaders/filmLook.d.ts +47 -0
- package/dist/render/shaders/filmLook.js +98 -0
- package/dist/render/shaders/flat/directionalShadow.js +35 -4
- package/dist/render/shaders/flat/driftLight.d.ts +29 -0
- package/dist/render/shaders/flat/driftLight.js +75 -0
- package/dist/render/shaders/flat/index.js +5 -0
- package/dist/render/shaders/flat/lobes.js +13 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +68 -9
- package/dist/render/shaders/flat/preamble.js +23 -5
- package/dist/render/shaders/flat/probeGrid.js +46 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
- package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
- package/dist/render/shaders/generated/depth.wgsl.js +100 -0
- package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
- package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
- package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
- package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
- package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
- package/dist/render/shaders/generated/rush.wgsl.js +40 -10
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
- package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
- package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
- package/dist/render/shaders/globalMedium.js +4 -0
- package/dist/render/shaders/localExposure.d.ts +35 -0
- package/dist/render/shaders/localExposure.js +138 -0
- package/dist/render/shaders/prefilter.d.ts +1 -1
- package/dist/render/shaders/prefilter.js +6 -0
- package/dist/render/shaders/rush.js +33 -5
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +15 -3
- package/dist/render/shaders/temporalResolve.js +39 -1
- package/dist/render/shadowCasters.d.ts +5 -3
- package/dist/render/shadowMap.js +13 -0
- package/dist/render/surfaceTexture.d.ts +5 -0
- package/package.json +6 -6
- package/scripts/gpuCompute.mjs +24 -3
- package/scripts/png.mjs +23 -12
- package/src/cinematic/cameraPath.ts +178 -0
- package/src/environment/daylight.ts +152 -0
- package/src/geometry/meshConcat.ts +100 -0
- package/src/index.ts +32 -0
- package/src/render/ambientOcclusionPass.ts +5 -1
- package/src/render/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +439 -20
- package/src/render/backend/webgpu/buffers.ts +51 -8
- package/src/render/backend/webgpu/depthPass.ts +78 -10
- package/src/render/backend/webgpu/exposurePass.ts +234 -0
- package/src/render/backend/webgpu/fieldCompose.ts +15 -0
- package/src/render/backend/webgpu/flatPass.ts +10 -2
- package/src/render/backend/webgpu/particlePass.ts +9 -0
- package/src/render/backend/webgpu/probeBake.ts +143 -9
- package/src/render/backend/webgpu/probeTargets.ts +211 -0
- package/src/render/backend/webgpu/renderer.ts +808 -273
- package/src/render/backend/webgpu/scatterPass.ts +7 -7
- package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
- package/src/render/backend/webgpu/skyPass.ts +5 -1
- package/src/render/backend/webgpu/uniformRing.ts +49 -3
- package/src/render/bloomChain.ts +9 -0
- package/src/render/clusteredLights.ts +108 -63
- package/src/render/cutoutCaster.ts +44 -0
- package/src/render/driftLight/bake.ts +125 -0
- package/src/render/driftLight/falloff.ts +26 -0
- package/src/render/driftLight/layout.ts +177 -0
- package/src/render/driftLight/lightField.ts +227 -0
- package/src/render/driftLight/uniforms.ts +61 -0
- package/src/render/driftLight/visibility.ts +58 -0
- package/src/render/emptyTexture.ts +41 -0
- package/src/render/exposurePass.ts +186 -0
- package/src/render/flameLight.ts +135 -0
- package/src/render/gi/bounceLights.ts +68 -0
- package/src/render/gi/fieldReach.ts +74 -0
- package/src/render/gi/globalField.ts +39 -0
- package/src/render/gi/probeTrace.ts +3 -1
- package/src/render/gpuCapability.ts +18 -5
- package/src/render/lightBudget.ts +12 -0
- package/src/render/pointLightSelection.ts +191 -79
- package/src/render/pointShadowBudget.ts +16 -8
- package/src/render/probeGrid.ts +13 -2
- package/src/render/probeSweeps.ts +80 -0
- package/src/render/reflectionProbe.ts +54 -2
- package/src/render/renderQuality.ts +3 -1
- package/src/render/rendererHarness.ts +25 -3
- package/src/render/sceneTarget.ts +75 -1
- package/src/render/shaders/ambientOcclusion.ts +205 -71
- package/src/render/shaders/cutoutCoverage.ts +43 -0
- package/src/render/shaders/depth.ts +80 -3
- package/src/render/shaders/exposure.ts +128 -0
- package/src/render/shaders/filmLook.ts +111 -0
- package/src/render/shaders/flat/directionalShadow.ts +35 -4
- package/src/render/shaders/flat/driftLight.ts +75 -0
- package/src/render/shaders/flat/index.ts +5 -0
- package/src/render/shaders/flat/lobes.ts +13 -0
- package/src/render/shaders/flat/main.ts +68 -9
- package/src/render/shaders/flat/preamble.ts +23 -5
- package/src/render/shaders/flat/probeGrid.ts +46 -3
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
- package/src/render/shaders/generated/depth.wgsl.ts +103 -0
- package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
- package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
- package/src/render/shaders/generated/rush.wgsl.ts +40 -10
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
- package/src/render/shaders/globalMedium.ts +4 -0
- package/src/render/shaders/localExposure.ts +154 -0
- package/src/render/shaders/prefilter.ts +6 -0
- package/src/render/shaders/rush.ts +33 -5
- package/src/render/shaders/tangentFrame.ts +15 -3
- package/src/render/shaders/temporalResolve.ts +39 -1
- package/src/render/shadowCasters.ts +5 -3
- package/src/render/shadowMap.ts +14 -0
- package/src/render/surfaceTexture.ts +5 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mat4 } from 'gl-matrix';
|
|
2
|
+
import { probeFaceRange } from '../../reflectionProbe.js';
|
|
2
3
|
import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
|
|
3
4
|
import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.js';
|
|
4
5
|
import { TranslucentQueue } from '../../translucentQueue.js';
|
|
@@ -15,7 +16,7 @@ import { PickableSet } from '../../pickable.js';
|
|
|
15
16
|
import { GpuTimestamps } from './gpuTimestamps.js';
|
|
16
17
|
import { isWeakGpuFamily } from '../../gpuCapability.js';
|
|
17
18
|
import { resolveAtmosphere } from '../../atmosphere.js';
|
|
18
|
-
import { BLOOM_FILTER_RADIUS_UV, BLOOM_LEVELS, bloomLevelSizes, bloomProfileWarning, } from '../../bloomChain.js';
|
|
19
|
+
import { BLOOM_FILTER_RADIUS_UV, BLOOM_LEVELS, bloomLevelSizes, bloomThresholdOf, bloomProfileWarning, } from '../../bloomChain.js';
|
|
19
20
|
import { createResolvedWater, resolveWater, waterAppearance, } from '../../waterDraw.js';
|
|
20
21
|
import { createResolvedLightVolume, DEPTH_01_TO_CLIP_Y_DOWN, resolveLightVolume, volumeMediumGain, } from '../../lightVolumeDraw.js';
|
|
21
22
|
import { WIND_STREAK_DEFAULTS, createResolvedWindStreaks, resolveWindStreaks, } from '../../windStreakDraw.js';
|
|
@@ -24,6 +25,7 @@ import { LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from '../
|
|
|
24
25
|
import { roughnessForLevel } from '../../prefilterEnvMap.js';
|
|
25
26
|
import { equirectToCubeFaces } from '../../equirectToCube.js';
|
|
26
27
|
import { MAX_ENV_PROBES, ProbeGrid, SINGLE_PROBE, UNIT_STEP, WORLD_ORIGIN, sameGrid, } from '../../probeGrid.js';
|
|
28
|
+
import { ProbeSweeps } from '../../probeSweeps.js';
|
|
27
29
|
import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.js';
|
|
28
30
|
import { environmentTexels } from './environmentTexels.js';
|
|
29
31
|
import { packIesAtlas } from '../../iesProfile.js';
|
|
@@ -45,9 +47,14 @@ import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG
|
|
|
45
47
|
import { reflectionTargetSize } from '../../planarReflectionDraw.js';
|
|
46
48
|
import { drawingBufferSize } from '../../drawingBuffer.js';
|
|
47
49
|
import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.js';
|
|
50
|
+
import { ExposurePass } from './exposurePass.js';
|
|
51
|
+
import { AO_STORE } from '../../shaders/ambientOcclusion.js';
|
|
52
|
+
import { clampAutoExposure } from '../../shaders/exposure.js';
|
|
53
|
+
import { clampLocalExposure } from '../../shaders/localExposure.js';
|
|
48
54
|
import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, ADDITIVE_BLEND, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
|
|
49
55
|
import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
|
|
50
|
-
import {
|
|
56
|
+
import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
|
|
57
|
+
import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
|
|
51
58
|
import { CAUSTICS_CELL_M, CAUSTICS_MAX_DROP_M } from '../../causticsRenderer.js';
|
|
52
59
|
import { buildSheets } from '../../surfaceSheet.js';
|
|
53
60
|
import { seaStateForWind } from '../../seaState.js';
|
|
@@ -62,13 +69,13 @@ import { DEFAULT_GLOBAL_MEDIUM, mediumActive, mediumTargetSize, WEAK_GPU_MEDIUM_
|
|
|
62
69
|
import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental } from './buffers.js';
|
|
63
70
|
import { SkinPaletteRing } from './skinPaletteRing.js';
|
|
64
71
|
import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, } from './flatPass.js';
|
|
65
|
-
import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
|
|
72
|
+
import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_CUTOUT_VERT_FIELDS, DEPTH_CUTOUT_VERT_SIZE, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
|
|
66
73
|
import { PipelineCache } from './pipelineCache.js';
|
|
67
74
|
import { SKY_FIELDS, SKY_UNIFORM_SIZE, createSkyBindGroup, createSkyBindGroupLayout, skyPipeline, } from './skyPass.js';
|
|
68
75
|
import { SCATTER_DEPTH_FIELDS, SCATTER_DEPTH_SIZE, SCATTER_FRAG_FIELDS, SCATTER_FRAG_SIZE, SCATTER_VERT_FIELDS, SCATTER_VERT_SIZE, createGpuScatter, createScatterBindGroup, createScatterBindGroupLayout, createScatterDepthBindGroup, createScatterDepthBindGroupLayout, scatterDepthPipeline, scatterPipeline, } from './scatterPass.js';
|
|
69
76
|
import { WATER_FRAG_FIELDS, WATER_FRAG_SIZE, WATER_VERT_FIELDS, WATER_VERT_SIZE, createGpuWater, createWaterBindGroup, createWaterBindGroupLayout, waterPipeline, } from './waterPass.js';
|
|
70
77
|
import { PLUME_FRAG_FIELDS, PLUME_FRAG_SIZE, PLUME_VERT_FIELDS, PLUME_VERT_SIZE, createGpuPlumes, createPlumeBindGroup, createPlumeBindGroupLayout, plumePipeline, } from './plumePass.js';
|
|
71
|
-
import { PARTICLE_VERT_FIELDS,
|
|
78
|
+
import { PARTICLE_VERT_FIELDS, createGpuParticles, createParticleBindGroupLayout, packInstances, particleFragBindings, particlePipeline, } from './particlePass.js';
|
|
72
79
|
import { WIND_STREAK_FRAG_FIELDS, WIND_STREAK_FRAG_SIZE, WIND_STREAK_VERT_FIELDS, WIND_STREAK_VERT_SIZE, createGpuWindStreaks, createWindStreakBindGroup, createWindStreakBindGroupLayout, windStreakPipeline, } from './windStreakPass.js';
|
|
73
80
|
import { FLOCK_FRAG_FIELDS, FLOCK_FRAG_SIZE, FLOCK_VERT_FIELDS, FLOCK_VERT_SIZE, createFlockBindGroup, createFlockBindGroupLayout, createGpuFlock, flockPipeline, } from './flockPass.js';
|
|
74
81
|
import { BOLT_FRAG_FIELDS, BOLT_FRAG_SIZE, BOLT_VERT_FIELDS, BOLT_VERT_SIZE, boltPipeline, createBoltBindGroup, createBoltBindGroupLayout, createGpuBolts, } from './boltPass.js';
|
|
@@ -80,8 +87,11 @@ import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
|
|
|
80
87
|
import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.js';
|
|
81
88
|
import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.js';
|
|
82
89
|
import { DistanceFieldScene } from '../../gi/fieldScene.js';
|
|
83
|
-
import { DEFAULT_FIELD_COMPOSE, FieldComposer } from './fieldCompose.js';
|
|
90
|
+
import { DEFAULT_FIELD_COMPOSE, FieldComposer, MAX_FIELD_CASCADES } from './fieldCompose.js';
|
|
91
|
+
import { cascadesForGrid } from '../../gi/fieldReach.js';
|
|
84
92
|
import { ProbeBaker } from './probeBake.js';
|
|
93
|
+
import { createBounceLights, resolveBounceLights } from '../../gi/bounceLights.js';
|
|
94
|
+
import { ProbeTargets } from './probeTargets.js';
|
|
85
95
|
import { distanceFieldBounds } from '../../gi/fieldScene.js';
|
|
86
96
|
import { fitProbeGrid } from '../../gi/probeGridFit.js';
|
|
87
97
|
/**
|
|
@@ -107,6 +117,9 @@ import { ClusterBinner } from './clusterBinner.js';
|
|
|
107
117
|
import { MAX_CLUSTERED_LIGHTS } from '../../clusteredLights.js';
|
|
108
118
|
import { computeAt, createComputeRegistry, } from '../../compute.js';
|
|
109
119
|
import { GpuInstancedBatch } from './instanced.js';
|
|
120
|
+
import { cutoutOf } from '../../cutoutCaster.js';
|
|
121
|
+
import { LightField } from '../../driftLight/lightField.js';
|
|
122
|
+
import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
|
|
110
123
|
/**
|
|
111
124
|
* How many draws one frame may make before the ring is full.
|
|
112
125
|
*
|
|
@@ -157,6 +170,12 @@ const MAX_SCATTER_DEPTH_DRAWS = 256;
|
|
|
157
170
|
* against the world.
|
|
158
171
|
*/
|
|
159
172
|
const MAX_OVERLAYS = 64;
|
|
173
|
+
/**
|
|
174
|
+
* Sky draws a frame can hold. A frame draws one, a mirror adds one, and a probe bake six a probe —
|
|
175
|
+
* given back after each probe's submit, so a whole grid needs six. Sixty-four is room for insets
|
|
176
|
+
* and passes nobody has written yet; the cost is sixteen kilobytes.
|
|
177
|
+
*/
|
|
178
|
+
const MAX_SKY_DRAWS = 64;
|
|
160
179
|
/**
|
|
161
180
|
* Distinct materials a frame may set.
|
|
162
181
|
*
|
|
@@ -678,8 +697,6 @@ const AO_BLUR_SLOT = DYNAMIC_UNIFORM_ALIGNMENT;
|
|
|
678
697
|
const BLOOM_UPSAMPLE_SLOT = BLOOM_LEVELS;
|
|
679
698
|
/** The tint a draw gets when it asks for none, matching the WebGL2 path's reset-to-white. */
|
|
680
699
|
const WHITE = new Float32Array([1, 1, 1]);
|
|
681
|
-
/** No surface-texture scaling, which is what an untextured mesh wants. */
|
|
682
|
-
const UNIT_UV = new Float32Array([1, 1]);
|
|
683
700
|
/** A dust field standing still, for a light volume that asked for dust and not for wind. */
|
|
684
701
|
const NO_DRIFT = new Float32Array([0, 0, 0]);
|
|
685
702
|
/**
|
|
@@ -728,6 +745,23 @@ const NO_CLIP_PLANE = new Float32Array(4);
|
|
|
728
745
|
* matrix the scene drew with and hands over the product.
|
|
729
746
|
*/
|
|
730
747
|
const MOTION_CLIP_FLIP = mat4.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
748
|
+
/* Scratch for the probe blur's and convolution's uniform writes, so a bake writes no array literal. */
|
|
749
|
+
const SCRATCH_ONE = new Float32Array(1);
|
|
750
|
+
const SCRATCH_VEC4 = new Float32Array(4);
|
|
751
|
+
const ZERO4 = new Float32Array(4);
|
|
752
|
+
/** `value` in a one-float scratch, overwritten by the next call. */
|
|
753
|
+
function one(value) {
|
|
754
|
+
SCRATCH_ONE[0] = value;
|
|
755
|
+
return SCRATCH_ONE;
|
|
756
|
+
}
|
|
757
|
+
/** A direction padded to a `vec4`, in a scratch overwritten by the next call. */
|
|
758
|
+
function padded(direction) {
|
|
759
|
+
SCRATCH_VEC4[0] = direction[0] ?? 0;
|
|
760
|
+
SCRATCH_VEC4[1] = direction[1] ?? 0;
|
|
761
|
+
SCRATCH_VEC4[2] = direction[2] ?? 0;
|
|
762
|
+
SCRATCH_VEC4[3] = 0;
|
|
763
|
+
return SCRATCH_VEC4;
|
|
764
|
+
}
|
|
731
765
|
export class WebGPURenderer {
|
|
732
766
|
/**
|
|
733
767
|
* Real timestamp queries, where the adapter offers them.
|
|
@@ -1508,12 +1542,20 @@ export class WebGPURenderer {
|
|
|
1508
1542
|
frameEye = new Float32Array(3);
|
|
1509
1543
|
/** The environment the frame was drawn with. See `bindMeshPass`. */
|
|
1510
1544
|
frameEnv = null;
|
|
1545
|
+
/**
|
|
1546
|
+
* The sky this frame drew, or null until it draws one. A traced probe's escaping rays read it,
|
|
1547
|
+
* so a frame that stops drawing a sky must not go on lighting its probes with the last one.
|
|
1548
|
+
*/
|
|
1549
|
+
frameSky = null;
|
|
1511
1550
|
/** That matrix with the clip-space correction folded in, rebuilt once a frame. */
|
|
1512
1551
|
correctedViewProj = new Float32Array(16);
|
|
1513
1552
|
skyLayout;
|
|
1553
|
+
/**
|
|
1554
|
+
* A slot a sky draw, as every per-draw resource here is: see `UniformRing` and the 2026-08-27 rule.
|
|
1555
|
+
* It was one buffer rewritten at each draw, which gave every sky in an encoder the last camera.
|
|
1556
|
+
*/
|
|
1514
1557
|
skyUniforms;
|
|
1515
|
-
|
|
1516
|
-
skyFloats = new Float32Array(this.skyStaging);
|
|
1558
|
+
skyFloats = new Float32Array(SKY_UNIFORM_SIZE / 4);
|
|
1517
1559
|
skyBindGroup;
|
|
1518
1560
|
/** The camera's inverse with the clip correction undone, rebuilt once a frame. */
|
|
1519
1561
|
skyInvViewProj = new Float32Array(16);
|
|
@@ -1774,35 +1816,75 @@ export class WebGPURenderer {
|
|
|
1774
1816
|
* prefix that has already been uploaded costs one write of unchanged bytes and cannot be
|
|
1775
1817
|
* wrong: a ring only ever appends within a frame.
|
|
1776
1818
|
*/
|
|
1819
|
+
/**
|
|
1820
|
+
* Every per-draw ring the frame writes and flushes together, in one list.
|
|
1821
|
+
*
|
|
1822
|
+
* **One list, read by the flush and by the probe bake's rewind**, so a ring added to one cannot be
|
|
1823
|
+
* forgotten by the other: a ring flushed but never rewound would let a grid bake spend it, and one
|
|
1824
|
+
* rewound but never flushed would upload nothing. Built on first use, after the constructor has
|
|
1825
|
+
* set every field; `growTo` replaces a ring's buffer, never the ring, so the list stays true.
|
|
1826
|
+
*/
|
|
1827
|
+
ringList = null;
|
|
1828
|
+
rings() {
|
|
1829
|
+
this.ringList ??= [
|
|
1830
|
+
this.perDraw,
|
|
1831
|
+
this.lightVolumeVertices,
|
|
1832
|
+
this.lightVolumeFragments,
|
|
1833
|
+
this.plumeVerts,
|
|
1834
|
+
this.plumeFrags,
|
|
1835
|
+
this.waterVerts,
|
|
1836
|
+
this.waterFrags,
|
|
1837
|
+
this.boltVerts,
|
|
1838
|
+
this.boltFrags,
|
|
1839
|
+
this.causticsVerts,
|
|
1840
|
+
this.causticsFrags,
|
|
1841
|
+
this.flockVerts,
|
|
1842
|
+
this.flockFrags,
|
|
1843
|
+
this.windStreakVerts,
|
|
1844
|
+
this.windStreakFrags,
|
|
1845
|
+
this.scatterVerts,
|
|
1846
|
+
this.scatterFrags,
|
|
1847
|
+
this.perFrame,
|
|
1848
|
+
this.filmVerts,
|
|
1849
|
+
this.filmFrags,
|
|
1850
|
+
this.insetUniforms,
|
|
1851
|
+
this.panelVerts,
|
|
1852
|
+
this.panelFrags,
|
|
1853
|
+
this.textVerts,
|
|
1854
|
+
this.textFrags,
|
|
1855
|
+
this.sdfTextVerts,
|
|
1856
|
+
this.sdfTextFrags,
|
|
1857
|
+
this.lineVerts,
|
|
1858
|
+
this.lineFrags,
|
|
1859
|
+
this.skyUniforms,
|
|
1860
|
+
];
|
|
1861
|
+
return this.ringList;
|
|
1862
|
+
}
|
|
1777
1863
|
flushRings() {
|
|
1778
|
-
this.
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
this.
|
|
1785
|
-
this.
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
this.
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
this.
|
|
1800
|
-
this.
|
|
1801
|
-
|
|
1802
|
-
this.sdfTextVerts.flush();
|
|
1803
|
-
this.sdfTextFrags.flush();
|
|
1804
|
-
this.lineVerts.flush();
|
|
1805
|
-
this.lineFrags.flush();
|
|
1864
|
+
for (const ring of this.rings())
|
|
1865
|
+
ring.flush();
|
|
1866
|
+
}
|
|
1867
|
+
/** Where every ring stands before work that submits an encoder of its own. */
|
|
1868
|
+
ringMarks = [];
|
|
1869
|
+
markRings() {
|
|
1870
|
+
const rings = this.rings();
|
|
1871
|
+
this.ringMarks.length = rings.length;
|
|
1872
|
+
for (let i = 0; i < rings.length; i++)
|
|
1873
|
+
this.ringMarks[i] = rings[i].mark();
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Give back every slot taken since `markRings`, once that work has been submitted.
|
|
1877
|
+
*
|
|
1878
|
+
* See `UniformRing.rewind` for why this is safe only after the submit. The palette ring goes back
|
|
1879
|
+
* with them, and the cached palette slot is dropped if it was one of those given back.
|
|
1880
|
+
*/
|
|
1881
|
+
rewindRings(skinMark) {
|
|
1882
|
+
const rings = this.rings();
|
|
1883
|
+
for (let i = 0; i < rings.length; i++)
|
|
1884
|
+
rings[i].rewind(this.ringMarks[i] ?? 0);
|
|
1885
|
+
this.skinPalettes.rewind(skinMark);
|
|
1886
|
+
if (this.skinPaletteSlot >= skinMark)
|
|
1887
|
+
this.skinPaletteSlot = -1;
|
|
1806
1888
|
}
|
|
1807
1889
|
/* -- The film ------------------------------------------------------------------------- */
|
|
1808
1890
|
filmLayout;
|
|
@@ -1970,6 +2052,8 @@ export class WebGPURenderer {
|
|
|
1970
2052
|
blankAlbedoBindGroup;
|
|
1971
2053
|
/** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
|
|
1972
2054
|
uvScale = new Float32Array([1, 1]);
|
|
2055
|
+
/** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
|
|
2056
|
+
materialDoubleSided = false;
|
|
1973
2057
|
createSurfaceTexture(source, options = {}) {
|
|
1974
2058
|
return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
|
|
1975
2059
|
}
|
|
@@ -2032,6 +2116,8 @@ export class WebGPURenderer {
|
|
|
2032
2116
|
* a group holding the old view hands a destroyed texture to the next draw that asks.
|
|
2033
2117
|
*/
|
|
2034
2118
|
forgetBindingsOf(texture) {
|
|
2119
|
+
this.cutoutGroups.delete(texture);
|
|
2120
|
+
this.cutoutPeelGroups.delete(texture);
|
|
2035
2121
|
/* Same reasoning, for whichever SDF labels were bound against this atlas. */
|
|
2036
2122
|
this.sdfTextBindGroups.delete(texture);
|
|
2037
2123
|
this.flatBindGroups.clear();
|
|
@@ -2088,12 +2174,14 @@ export class WebGPURenderer {
|
|
|
2088
2174
|
const albedo = material?.albedo ?? null;
|
|
2089
2175
|
this.uvScale[0] = material?.uScale ?? 1;
|
|
2090
2176
|
this.uvScale[1] = material?.vScale ?? 1;
|
|
2177
|
+
this.materialDoubleSided = material?.doubleSided === true;
|
|
2091
2178
|
const normal = (material?.normal ?? null);
|
|
2092
2179
|
const orm = (material?.orm ?? null);
|
|
2093
2180
|
const emissiveMap = (material?.emissive ?? null);
|
|
2094
2181
|
this.material((f, i) => {
|
|
2095
2182
|
i[this.materialField('uAlbedoEnabled')] = albedo === null ? 0 : 1;
|
|
2096
2183
|
f[this.materialField('uAlbedoCutout')] = material?.cutout ?? 0;
|
|
2184
|
+
i[this.materialField('uDoubleSided')] = material?.doubleSided === true ? 1 : 0;
|
|
2097
2185
|
/* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
|
|
2098
2186
|
f[this.materialField('uNormalStrength')] =
|
|
2099
2187
|
normal === null ? 0 : (material?.normalStrength ?? 1);
|
|
@@ -2862,6 +2950,16 @@ export class WebGPURenderer {
|
|
|
2862
2950
|
drawParticles(batch, data, camera, env, timeSeconds) {
|
|
2863
2951
|
if (!this.canDraw() || data.count === 0)
|
|
2864
2952
|
return;
|
|
2953
|
+
if (batch.drawnInEncoder === this.encoderEpoch) {
|
|
2954
|
+
if (!this.warnedParticlesTwice) {
|
|
2955
|
+
this.warnedParticlesTwice = true;
|
|
2956
|
+
console.warn('WebGPU: a particle batch was drawn twice into one encoder — a mirror and the view, or a ' +
|
|
2957
|
+
"probe's faces — and the second draw is skipped, because both would show the second " +
|
|
2958
|
+
"draw's particles. Give each view its own batch.");
|
|
2959
|
+
}
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
batch.drawnInEncoder = this.encoderEpoch;
|
|
2865
2963
|
const live = packInstances(batch, data);
|
|
2866
2964
|
if (live === 0)
|
|
2867
2965
|
return;
|
|
@@ -3488,7 +3586,6 @@ export class WebGPURenderer {
|
|
|
3488
3586
|
v[at('uCellSize')] = streaks.cellSize;
|
|
3489
3587
|
v[at('uTime')] = timeSeconds;
|
|
3490
3588
|
this.windStreakFragFloats.set(tint, (WIND_STREAK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
|
|
3491
|
-
const { queue } = this.surface.device;
|
|
3492
3589
|
this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
|
|
3493
3590
|
this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
|
|
3494
3591
|
const windPipe = windStreakPipeline(this.pipelines, this.surface.device, this.windStreakLayout);
|
|
@@ -3574,7 +3671,6 @@ export class WebGPURenderer {
|
|
|
3574
3671
|
v[at('uWind')] = windX;
|
|
3575
3672
|
v[at('uWind') + 1] = windZ;
|
|
3576
3673
|
this.flockFragFloats.set(tint, (FLOCK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
|
|
3577
|
-
const { queue } = this.surface.device;
|
|
3578
3674
|
this.flockVerts.writeBlock(vertexSlot, this.flockVertBlock);
|
|
3579
3675
|
this.flockFrags.writeBlock(fragmentSlot, this.flockFragBlock);
|
|
3580
3676
|
const flockPipe = flockPipeline(this.pipelines, this.surface.device, this.flockLayout);
|
|
@@ -3868,7 +3964,8 @@ export class WebGPURenderer {
|
|
|
3868
3964
|
scatterLayout;
|
|
3869
3965
|
/** One slot per batch: the gust and the trample field belong to the batch, not the frame. */
|
|
3870
3966
|
scatterVerts;
|
|
3871
|
-
|
|
3967
|
+
/** A slot a draw, beside the vertex block's: see `createScatterBindGroupLayout` for why. */
|
|
3968
|
+
scatterFrags;
|
|
3872
3969
|
scatterFragStaging = new ArrayBuffer(SCATTER_FRAG_SIZE);
|
|
3873
3970
|
scatterFragFloats = new Float32Array(this.scatterFragStaging);
|
|
3874
3971
|
scatterFragInts = new Int32Array(this.scatterFragStaging);
|
|
@@ -4053,17 +4150,28 @@ export class WebGPURenderer {
|
|
|
4053
4150
|
dynamicView;
|
|
4054
4151
|
/** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
|
|
4055
4152
|
shadowPeelBindGroup;
|
|
4153
|
+
/**
|
|
4154
|
+
* The cutout casters' layout, and a bind group per cutout map for each layer, built the first
|
|
4155
|
+
* time a map casts and kept: a `createBindGroup` per draw is the allocation the house rules forbid.
|
|
4156
|
+
* Cleared in `forgetBindingsOf`, because a group outliving its texture is a destroyed-texture error
|
|
4157
|
+
* at submit, and a texture's view is new when an update changes its size.
|
|
4158
|
+
*/
|
|
4159
|
+
shadowCutoutLayout;
|
|
4160
|
+
cutoutGroups = new Map();
|
|
4161
|
+
cutoutPeelGroups = new Map();
|
|
4162
|
+
shadowPeelPassUniforms = null;
|
|
4163
|
+
shadowPeelSampler = null;
|
|
4056
4164
|
scatterDepthPeelBindGroup;
|
|
4057
4165
|
/** Which layer the open pass is writing, so the sink picks the bindings that match it. */
|
|
4058
4166
|
shadowLayerIsPeel = false;
|
|
4059
4167
|
/**
|
|
4060
4168
|
* Whether a peel pass has filled the map, which is what the frame may sample it on.
|
|
4061
4169
|
*
|
|
4062
|
-
* **Not whether the map exists.** `renderer.ts`
|
|
4063
|
-
* born
|
|
4064
|
-
*
|
|
4065
|
-
* static layer opens and set when a peel pass ends, which
|
|
4066
|
-
*
|
|
4170
|
+
* **Not whether the map exists.** `renderer.ts` asked that question, on the belief that a GL
|
|
4171
|
+
* texture is born reading as the far plane, and it is born zeroed: a scene that built a peel
|
|
4172
|
+
* map and never opened the layer had no sun at all under ANGLE's GL backend. It holds this
|
|
4173
|
+
* rule now as well. Cleared when the static layer opens and set when a peel pass ends, which
|
|
4174
|
+
* is per frame because every scene that peels does so every frame.
|
|
4067
4175
|
*/
|
|
4068
4176
|
peelFilled = false;
|
|
4069
4177
|
/** One slot per caster, the same trick `perDraw` uses and for the same reason. */
|
|
@@ -4104,6 +4212,28 @@ export class WebGPURenderer {
|
|
|
4104
4212
|
* otherwise. A lookup rather than a rebind, because a `createBindGroup` per draw is the
|
|
4105
4213
|
* per-frame allocation the house rules forbid; both groups are built once at init.
|
|
4106
4214
|
*/
|
|
4215
|
+
/** The cutout bind group for `texture` in the open layer, built the first time it casts there. */
|
|
4216
|
+
cutoutGroup(texture) {
|
|
4217
|
+
const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
|
|
4218
|
+
const groups = peel ? this.cutoutPeelGroups : this.cutoutGroups;
|
|
4219
|
+
const held = groups.get(texture);
|
|
4220
|
+
if (held !== undefined)
|
|
4221
|
+
return held;
|
|
4222
|
+
const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
|
|
4223
|
+
const previousView = peel ? this.shadowView : this.shadowBlankView;
|
|
4224
|
+
const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
|
|
4225
|
+
if (passUniforms === null || previousView === null || previousSampler === null)
|
|
4226
|
+
return null;
|
|
4227
|
+
const group = createDepthBindGroup(this.surface.device, this.shadowCutoutLayout, this.shadowDraws.buffer, passUniforms, previousView, previousSampler, null, { view: texture.view, sampler: texture.sampler });
|
|
4228
|
+
groups.set(texture, group);
|
|
4229
|
+
return group;
|
|
4230
|
+
}
|
|
4231
|
+
/** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
|
|
4232
|
+
writeCutout(slot, cutout, fields) {
|
|
4233
|
+
this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
|
|
4234
|
+
this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
|
|
4235
|
+
this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
|
|
4236
|
+
}
|
|
4107
4237
|
shadowGroup() {
|
|
4108
4238
|
return this.shadowLayerIsPeel && this.shadowPeelBindGroup !== null
|
|
4109
4239
|
? this.shadowPeelBindGroup
|
|
@@ -4139,21 +4269,29 @@ export class WebGPURenderer {
|
|
|
4139
4269
|
* every frame would be an allocation in a per-frame path.
|
|
4140
4270
|
*/
|
|
4141
4271
|
casterSink = {
|
|
4142
|
-
mesh: (mesh, model) => {
|
|
4272
|
+
mesh: (mesh, model, material) => {
|
|
4143
4273
|
const pass = this.shadowPass;
|
|
4144
4274
|
if (pass === null)
|
|
4145
4275
|
return;
|
|
4276
|
+
/* A cutout caster's group is looked up before a slot is spent, so a miss costs nothing. */
|
|
4277
|
+
const cutout = cutoutOf(material);
|
|
4278
|
+
const cutoutGroup = cutout === null ? null : this.cutoutGroup(cutout.albedo);
|
|
4146
4279
|
const slot = this.shadowDraws.allocate();
|
|
4147
4280
|
if (slot === null)
|
|
4148
4281
|
return;
|
|
4149
4282
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
|
|
4150
4283
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model);
|
|
4151
4284
|
this.writeShadowWind(slot);
|
|
4285
|
+
if (cutout !== null && cutoutGroup !== null)
|
|
4286
|
+
this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
|
|
4152
4287
|
const geometry = mesh;
|
|
4153
4288
|
if (geometry.vertexBuffers === undefined)
|
|
4154
4289
|
return;
|
|
4155
|
-
|
|
4156
|
-
|
|
4290
|
+
const cuts = cutoutGroup !== null;
|
|
4291
|
+
/* A two-sided caster casts from whichever face the light sees: nothing culled. */
|
|
4292
|
+
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
4293
|
+
pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, cuts ? this.shadowCutoutLayout : this.shadowLayout, `${cuts ? 'depth-cutout' : 'depth'}|${cull}|${geometry.key ?? ''}`, this.presentOf(geometry), cull, false, false, cuts));
|
|
4294
|
+
pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
|
|
4157
4295
|
for (let index = 0; index < geometry.vertexBuffers.length; index++) {
|
|
4158
4296
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
|
|
4159
4297
|
}
|
|
@@ -4164,7 +4302,7 @@ export class WebGPURenderer {
|
|
|
4164
4302
|
* A batch of rigid meshes into the depth map, placed by the same matrices the visible draw
|
|
4165
4303
|
* uses — one draw, however many instances, exactly as the colour pass does it.
|
|
4166
4304
|
*/
|
|
4167
|
-
instanced: (batch, data) => {
|
|
4305
|
+
instanced: (batch, data, material) => {
|
|
4168
4306
|
const pass = this.shadowPass;
|
|
4169
4307
|
if (pass === null)
|
|
4170
4308
|
return;
|
|
@@ -4175,14 +4313,21 @@ export class WebGPURenderer {
|
|
|
4175
4313
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
4176
4314
|
if (count === 0)
|
|
4177
4315
|
return;
|
|
4316
|
+
const cutout = cutoutOf(material);
|
|
4317
|
+
const cutoutGroup = cutout === null ? null : this.cutoutGroup(cutout.albedo);
|
|
4178
4318
|
const slot = this.shadowDraws.allocate();
|
|
4179
4319
|
if (slot === null)
|
|
4180
4320
|
return;
|
|
4181
4321
|
/* Only the light matrix: the instanced depth variant has no uModel, its placement being
|
|
4182
4322
|
the four attribute columns the batch's buffer supplies. */
|
|
4183
4323
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
|
|
4184
|
-
|
|
4185
|
-
|
|
4324
|
+
if (cutout !== null && cutoutGroup !== null) {
|
|
4325
|
+
this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
|
|
4326
|
+
}
|
|
4327
|
+
const cuts = cutoutGroup !== null;
|
|
4328
|
+
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
4329
|
+
pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, cuts ? this.shadowCutoutLayout : this.shadowLayout, `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}|${cull}|${geometry.key ?? ''}`, this.presentOf(geometry), cull, false, true, cuts));
|
|
4330
|
+
pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
|
|
4186
4331
|
let index = 0;
|
|
4187
4332
|
for (; index < geometry.vertexBuffers.length; index++) {
|
|
4188
4333
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
|
|
@@ -4611,28 +4756,6 @@ export class WebGPURenderer {
|
|
|
4611
4756
|
*/
|
|
4612
4757
|
const blankView = blank.createView();
|
|
4613
4758
|
const blankSampler = device.createSampler({ label: 'flat.sampler' });
|
|
4614
|
-
/*
|
|
4615
|
-
* A one-texel cube for every point-shadow sampler the variant declares.
|
|
4616
|
-
*
|
|
4617
|
-
* Twelve of them exist in the `pointShadows` permutation and none has a map behind it
|
|
4618
|
-
* until row 3, but WebGPU requires every declared binding to be filled — and a cube view
|
|
4619
|
-
* needs six array layers, so the flat 1×1 stand-in above cannot serve.
|
|
4620
|
-
*
|
|
4621
|
-
* **White is the second guard, and it is a deliberate value rather than a spare one.** The
|
|
4622
|
-
* first is `uPointShadowIndex`, written to −1 for every light in `bindMeshPass`. If that
|
|
4623
|
-
* ever failed, a full-depth texel reads as an occluder at the far plane — `pointShadow`
|
|
4624
|
-
* converts the stored value back to a distance and 1.0 comes out as `far` — so the worst a
|
|
4625
|
-
* mis-selected light can do is find nothing in the way. Zeroes would have meant an
|
|
4626
|
-
* occluder pressed against the lamp, and every surface in the scene in shadow.
|
|
4627
|
-
*/
|
|
4628
|
-
const blankCube = device.createTexture({
|
|
4629
|
-
label: 'flat.emptyPointShadow',
|
|
4630
|
-
size: [1, 1, 6],
|
|
4631
|
-
format: 'rgba8unorm',
|
|
4632
|
-
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
4633
|
-
});
|
|
4634
|
-
device.queue.writeTexture({ texture: blankCube }, new Uint8Array(4 * 6).fill(255), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 6]);
|
|
4635
|
-
const blankCubeView = blankCube.createView({ dimension: 'cube' });
|
|
4636
4759
|
/*
|
|
4637
4760
|
* A one-texel integer stand-in for the froxel table.
|
|
4638
4761
|
*
|
|
@@ -4671,6 +4794,16 @@ export class WebGPURenderer {
|
|
|
4671
4794
|
});
|
|
4672
4795
|
device.queue.writeTexture({ texture: blankArray }, new Uint8Array(4).fill(255), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 1]);
|
|
4673
4796
|
const blankArrayView = blankArray.createView({ dimension: '2d-array' });
|
|
4797
|
+
/*
|
|
4798
|
+
* DriftLight's two volumes before a field is whole: an index of one empty cell and an atlas
|
|
4799
|
+
* of one zero texel. Never read while `uDriftLight.x` is zero, and bound anyway, because a
|
|
4800
|
+
* group missing an entry its layout declares is a validation failure at `submit`.
|
|
4801
|
+
*/
|
|
4802
|
+
const blankVolume = (label, format) => device
|
|
4803
|
+
.createTexture({ label, size: [1, 1, 1], dimension: '3d', format, usage: 0x2 | 0x4 })
|
|
4804
|
+
.createView({ dimension: '3d' });
|
|
4805
|
+
const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
|
|
4806
|
+
const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
|
|
4674
4807
|
this.shadowMap = device.createTexture({
|
|
4675
4808
|
label: 'shadow.static',
|
|
4676
4809
|
size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
|
|
@@ -4708,14 +4841,15 @@ export class WebGPURenderer {
|
|
|
4708
4841
|
: null;
|
|
4709
4842
|
this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
|
|
4710
4843
|
/*
|
|
4711
|
-
* **Cleared once, here, because a texture no pass has written
|
|
4844
|
+
* **Cleared once, here, because a texture no pass has written reads as zero**, which the
|
|
4845
|
+
* specification requires, and a zero in a map compared with `LEQUAL` is an occluder at the
|
|
4846
|
+
* light. A scene is entitled to switch directional shadows *on* and never open a pass —
|
|
4847
|
+
* `night-street` does exactly that, with `shadowStrength` at 0.85 and no `beginShadowPass`
|
|
4848
|
+
* anywhere in it — and sampling that zero shadows everything, which is a picture rather than
|
|
4849
|
+
* an error and reads as a lighting bug.
|
|
4712
4850
|
*
|
|
4713
|
-
*
|
|
4714
|
-
*
|
|
4715
|
-
* no such promise, and a scene is entitled to switch directional shadows *on* and never
|
|
4716
|
-
* open a pass — `night-street` does exactly that, with `shadowStrength` at 0.85 and no
|
|
4717
|
-
* `beginShadowPass` anywhere in it. Sampling garbage then shadows whatever the garbage
|
|
4718
|
-
* happens to say, which is a picture rather than an error and reads as a lighting bug.
|
|
4851
|
+
* `shadowMap.ts` does the same for the other backend, for the same reason found later: WebGL
|
|
4852
|
+
* zeroes a new texture too, and only ANGLE on Vulkan happened to hand one back reading 1.
|
|
4719
4853
|
*
|
|
4720
4854
|
* Found by dumping the flat block off both live renderers and comparing it field by field;
|
|
4721
4855
|
* `uPeeledShadowEnabled` came back 1 against 0 and pointed straight at the maps behind it.
|
|
@@ -4826,7 +4960,7 @@ export class WebGPURenderer {
|
|
|
4826
4960
|
* is writing, so the binding has to move whatever the shader intends to do with it.
|
|
4827
4961
|
*/
|
|
4828
4962
|
if (name === 'uEnvironment') {
|
|
4829
|
-
return this.probeView === null || !this.
|
|
4963
|
+
return this.probeView === null || !this.probeReadable()
|
|
4830
4964
|
? { view: blankArrayView, sampler: blankSampler }
|
|
4831
4965
|
: { view: this.probeView, sampler: this.probeSampler };
|
|
4832
4966
|
}
|
|
@@ -4860,6 +4994,17 @@ export class WebGPURenderer {
|
|
|
4860
4994
|
if (name === 'uCookieAtlas') {
|
|
4861
4995
|
return { view: this.cookieView ?? blankView, sampler: iesSampler };
|
|
4862
4996
|
}
|
|
4997
|
+
/*
|
|
4998
|
+
* DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
|
|
4999
|
+
* sampler the way the froxel table does; and its atlas, which is filtered inside a brick and
|
|
5000
|
+
* so takes the linear, clamped one the photometric atlas uses.
|
|
5001
|
+
*/
|
|
5002
|
+
if (name === 'uDriftLightIndex') {
|
|
5003
|
+
return { view: this.driftIndexView ?? blankDriftIndexView, sampler: shadowSampler };
|
|
5004
|
+
}
|
|
5005
|
+
if (name === 'uDriftLightAtlas') {
|
|
5006
|
+
return { view: this.driftAtlasView ?? blankDriftAtlasView, sampler: iesSampler };
|
|
5007
|
+
}
|
|
4863
5008
|
/*
|
|
4864
5009
|
* The colour the frame had already drawn, or the blank texel a frame that refracts nothing
|
|
4865
5010
|
* falls back to.
|
|
@@ -4914,12 +5059,8 @@ export class WebGPURenderer {
|
|
|
4914
5059
|
/* The same group: no texture is set at construction, so this is the stand-in's. */
|
|
4915
5060
|
this.blankAlbedoBindGroup = this.bindGroup;
|
|
4916
5061
|
this.skyLayout = createSkyBindGroupLayout(device);
|
|
4917
|
-
this.skyUniforms = device.
|
|
4918
|
-
|
|
4919
|
-
size: SKY_UNIFORM_SIZE,
|
|
4920
|
-
usage: USAGE_UNIFORM_DST,
|
|
4921
|
-
});
|
|
4922
|
-
this.skyBindGroup = createSkyBindGroup(device, this.skyLayout, this.skyUniforms);
|
|
5062
|
+
this.skyUniforms = new UniformRing(device, SKY_UNIFORM_SIZE, MAX_SKY_DRAWS, USAGE_UNIFORM_DST, 'sky.ring');
|
|
5063
|
+
this.skyBindGroup = createSkyBindGroup(device, this.skyLayout, this.skyUniforms.buffer);
|
|
4923
5064
|
this.plumeLayout = createPlumeBindGroupLayout(device);
|
|
4924
5065
|
this.plumeVerts = new UniformRing(device, PLUME_VERT_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
|
|
4925
5066
|
this.plumeFrags = new UniformRing(device, PLUME_FRAG_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
|
|
@@ -4958,6 +5099,8 @@ export class WebGPURenderer {
|
|
|
4958
5099
|
usage: 0x10 | 0x4 | 0x2, // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_DST
|
|
4959
5100
|
})
|
|
4960
5101
|
: null;
|
|
5102
|
+
this.probeTargets =
|
|
5103
|
+
this.probe === null ? null : new ProbeTargets(device, this.probe, this.probeSize);
|
|
4961
5104
|
/*
|
|
4962
5105
|
* The prefiltered twin, allocated on exactly the condition the capture is and with exactly
|
|
4963
5106
|
* its format and level count. Same format because a pipeline's colour target must equal its
|
|
@@ -5001,6 +5144,10 @@ export class WebGPURenderer {
|
|
|
5001
5144
|
is not a screen-sized target. Filtering is what makes a 32-lattice table smooth rather
|
|
5002
5145
|
than banded, so it is declared filterable like the colour inputs and unlike the depth. */
|
|
5003
5146
|
{ binding: RUSH_TEXTURES.uGradeLut, dimension: '3d' },
|
|
5147
|
+
/* Eye adaptation's held brightness: one half-float texel, or the scene where none is held. */
|
|
5148
|
+
{ binding: RUSH_TEXTURES.uExposureHeld },
|
|
5149
|
+
/* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
|
|
5150
|
+
{ binding: RUSH_TEXTURES.uExposureLocal },
|
|
5004
5151
|
]);
|
|
5005
5152
|
this.rushUniforms = device.createBuffer({
|
|
5006
5153
|
label: 'post.rushUniforms',
|
|
@@ -5167,15 +5314,14 @@ export class WebGPURenderer {
|
|
|
5167
5314
|
this.filmBindGroupBlank = this.filmBindGroup;
|
|
5168
5315
|
this.scatterLayout = createScatterBindGroupLayout(device);
|
|
5169
5316
|
this.scatterVerts = new UniformRing(device, SCATTER_VERT_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
|
|
5170
|
-
this.
|
|
5171
|
-
|
|
5172
|
-
size: SCATTER_FRAG_SIZE,
|
|
5173
|
-
usage: USAGE_UNIFORM_DST,
|
|
5174
|
-
});
|
|
5175
|
-
this.scatterBindGroup = createScatterBindGroup(device, this.scatterLayout, this.scatterVerts.buffer, this.scatterFragUniforms);
|
|
5317
|
+
this.scatterFrags = new UniformRing(device, SCATTER_FRAG_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST, 'scatter.fragRing');
|
|
5318
|
+
this.scatterBindGroup = createScatterBindGroup(device, this.scatterLayout, this.scatterVerts.buffer, this.scatterFrags.buffer);
|
|
5176
5319
|
this.shadowLayout = createDepthBindGroupLayout(device);
|
|
5177
5320
|
this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
|
|
5178
|
-
this.
|
|
5321
|
+
this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
|
|
5322
|
+
this.shadowDraws = new UniformRing(device,
|
|
5323
|
+
/* The cutout block is the plain one and two fields more, and a slot must hold either. */
|
|
5324
|
+
Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE), MAX_DRAWS_PER_FRAME, USAGE_UNIFORM_DST, 'shadow.drawRing', this.shadowDrawBudget);
|
|
5179
5325
|
const depthPassUniforms = device.createBuffer({
|
|
5180
5326
|
label: 'depth.passUniforms',
|
|
5181
5327
|
size: DEPTH_FRAG_SIZE,
|
|
@@ -5213,6 +5359,8 @@ export class WebGPURenderer {
|
|
|
5213
5359
|
if (peelPassUniforms !== null) {
|
|
5214
5360
|
device.queue.writeBuffer(peelPassUniforms, 0, new Int32Array([1]));
|
|
5215
5361
|
}
|
|
5362
|
+
this.shadowPeelPassUniforms = peelPassUniforms;
|
|
5363
|
+
this.shadowPeelSampler = shadowSampler;
|
|
5216
5364
|
this.shadowPeelBindGroup =
|
|
5217
5365
|
peelPassUniforms === null
|
|
5218
5366
|
? null
|
|
@@ -5221,13 +5369,6 @@ export class WebGPURenderer {
|
|
|
5221
5369
|
peelPassUniforms === null
|
|
5222
5370
|
? null
|
|
5223
5371
|
: createScatterDepthBindGroup(device, this.scatterDepthLayout, this.scatterDepthDraws.buffer, { binding: DEPTH_FRAG_BINDING, buffer: peelPassUniforms, size: DEPTH_FRAG_SIZE }, DEPTH_PREVIOUS_BINDING, this.shadowView, shadowSampler);
|
|
5224
|
-
/*
|
|
5225
|
-
* The light volume, last because it wants the shadow map and the stand-in that everything
|
|
5226
|
-
* above already built. The peel and dynamic maps get the white one-pixel texture, which is
|
|
5227
|
-
* depth 1 and therefore nothing in the way — see `createLightVolumeBindGroup` for why a
|
|
5228
|
-
* stand-in of zero would put every beam in shadow instead.
|
|
5229
|
-
*/
|
|
5230
|
-
const uniformBuffer = (label, size) => device.createBuffer({ label, size, usage: USAGE_UNIFORM_DST });
|
|
5231
5372
|
/*
|
|
5232
5373
|
* The overlays. Rings rather than single blocks because a frame draws several of each —
|
|
5233
5374
|
* the loader alone is two panels and two strings — and `queue.writeBuffer` does not
|
|
@@ -5389,6 +5530,7 @@ export class WebGPURenderer {
|
|
|
5389
5530
|
this.reflectionQueue.reset();
|
|
5390
5531
|
/* Redeclared every frame, so a field a consumer stops declaring stops lighting. */
|
|
5391
5532
|
this.distanceFields.reset();
|
|
5533
|
+
this.frameSky = null;
|
|
5392
5534
|
this.oitActive =
|
|
5393
5535
|
this.quality.orderIndependent && this.quality.screenEffects && this.samples === 1;
|
|
5394
5536
|
/* Read off the line above rather than restating its last term: with the other two true, the
|
|
@@ -5410,6 +5552,7 @@ export class WebGPURenderer {
|
|
|
5410
5552
|
/* Anything drawn after the last `endFrame` goes now, before its encoder is replaced. */
|
|
5411
5553
|
this.flushOverlay();
|
|
5412
5554
|
this.framePresented = false;
|
|
5555
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
5413
5556
|
/* Before the budget is cleared, because what to grow to is what the last frame asked for. */
|
|
5414
5557
|
this.growRings();
|
|
5415
5558
|
this.budget.reset();
|
|
@@ -5449,6 +5592,7 @@ export class WebGPURenderer {
|
|
|
5449
5592
|
this.windStreakVerts.reset();
|
|
5450
5593
|
this.windStreakFrags.reset();
|
|
5451
5594
|
this.scatterVerts.reset();
|
|
5595
|
+
this.scatterFrags.reset();
|
|
5452
5596
|
this.panelVerts.reset();
|
|
5453
5597
|
this.panelFrags.reset();
|
|
5454
5598
|
this.filmVerts.reset();
|
|
@@ -5460,6 +5604,7 @@ export class WebGPURenderer {
|
|
|
5460
5604
|
this.sdfTextFrags.reset();
|
|
5461
5605
|
this.lineVerts.reset();
|
|
5462
5606
|
this.lineFrags.reset();
|
|
5607
|
+
this.skyUniforms.reset();
|
|
5463
5608
|
/* The material ring, and the open slot with it: last frame's slots are gone. */
|
|
5464
5609
|
this.perFrame.reset();
|
|
5465
5610
|
this.materials.dirty();
|
|
@@ -5993,9 +6138,24 @@ export class WebGPURenderer {
|
|
|
5993
6138
|
f[base + 1] = counts[1];
|
|
5994
6139
|
f[base + 2] = counts[2];
|
|
5995
6140
|
}
|
|
6141
|
+
/* Which sets the shading reads, and how far between them. See `probeSweeps.ts`. */
|
|
6142
|
+
const setsAt = this.fragment.fields['uProbeGridSets'];
|
|
6143
|
+
if (setsAt !== undefined) {
|
|
6144
|
+
const sets = this.probeSweeps?.uniforms(this.probeSets) ?? this.probeSets.fill(0);
|
|
6145
|
+
const base = setsAt.offset / 4;
|
|
6146
|
+
f[base] = sets[0] ?? 0;
|
|
6147
|
+
f[base + 1] = sets[1] ?? 0;
|
|
6148
|
+
f[base + 2] = sets[2] ?? 0;
|
|
6149
|
+
}
|
|
5996
6150
|
/* Which chain is bound, because the level and the weight both depend on it. */
|
|
5997
6151
|
f[at('uEnvironmentPrefiltered')] = this.quality.environmentPrefilter ? 1 : 0;
|
|
5998
6152
|
}
|
|
6153
|
+
/* DriftLight, or zeros that switch it off: see `driftLight/uniforms.ts`. */
|
|
6154
|
+
this.uploadLightField();
|
|
6155
|
+
const drift = this.driftUniforms;
|
|
6156
|
+
resolveDriftLight(this.driftAtlas === null ? null : this.lightField, this.probePassActive, camera.position, drift);
|
|
6157
|
+
f.set(drift.light, at('uDriftLight'));
|
|
6158
|
+
f.set(drift.origin, at('uDriftLightOrigin'));
|
|
5999
6159
|
/*
|
|
6000
6160
|
* The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
|
|
6001
6161
|
*
|
|
@@ -6074,7 +6234,7 @@ export class WebGPURenderer {
|
|
|
6074
6234
|
* it every frame.
|
|
6075
6235
|
*/
|
|
6076
6236
|
if (this.probeEnabled) {
|
|
6077
|
-
const usable = this.
|
|
6237
|
+
const usable = this.probeReadable();
|
|
6078
6238
|
f[at('uEnvironmentEnabled')] = usable ? 1 : 0;
|
|
6079
6239
|
f[at('uEnvironmentMaxLod')] = this.probeMaxLod;
|
|
6080
6240
|
/*
|
|
@@ -6310,6 +6470,14 @@ export class WebGPURenderer {
|
|
|
6310
6470
|
}
|
|
6311
6471
|
/** Said once rather than every frame, for the reason `warnedFull` gives. */
|
|
6312
6472
|
warnedMaterialsFull = false;
|
|
6473
|
+
warnedSkyFull = false;
|
|
6474
|
+
warnedParticlesTwice = false;
|
|
6475
|
+
/**
|
|
6476
|
+
* Which encoder draws are being recorded into, by count: a new one at every `beginFrame`, and one
|
|
6477
|
+
* of its own for each probe bake, restored after. What a batch drawn per encoder is checked by.
|
|
6478
|
+
*/
|
|
6479
|
+
encoderEpoch = 0;
|
|
6480
|
+
encoderEpochs = 0;
|
|
6313
6481
|
/**
|
|
6314
6482
|
* Draw one mesh.
|
|
6315
6483
|
*
|
|
@@ -6593,7 +6761,9 @@ export class WebGPURenderer {
|
|
|
6593
6761
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
6594
6762
|
/* The two order-independent buffers are two formats and two blend states, so a draw into
|
|
6595
6763
|
one must never share a cached pipeline with the same mesh drawn into the other. */
|
|
6596
|
-
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}
|
|
6764
|
+
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
|
|
6765
|
+
/* Culling is pipeline state here: a two-sided surface culls nothing. */
|
|
6766
|
+
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
6597
6767
|
/*
|
|
6598
6768
|
* The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
|
|
6599
6769
|
* field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
|
|
@@ -6623,7 +6793,7 @@ export class WebGPURenderer {
|
|
|
6623
6793
|
if (present === undefined) {
|
|
6624
6794
|
throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
|
|
6625
6795
|
}
|
|
6626
|
-
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, blend, skinned, morphed, depthWrite, layer, false, this.oitMode);
|
|
6796
|
+
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided);
|
|
6627
6797
|
}
|
|
6628
6798
|
/*
|
|
6629
6799
|
* Guards the draw call only, not the restore below.
|
|
@@ -6829,7 +6999,8 @@ export class WebGPURenderer {
|
|
|
6829
6999
|
a third vertex buffer and a different vertex module, and sharing a cache entry with the
|
|
6830
7000
|
plain one would hand a draw the wrong layout. */
|
|
6831
7001
|
const key = `${base}|inst${blend ? '|blend' : ''}` +
|
|
6832
|
-
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}
|
|
7002
|
+
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
7003
|
+
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
6833
7004
|
const pipelines = this.targetPipelines();
|
|
6834
7005
|
let pipeline = pipelines.peek(key);
|
|
6835
7006
|
if (pipeline === undefined) {
|
|
@@ -6837,7 +7008,7 @@ export class WebGPURenderer {
|
|
|
6837
7008
|
if (present === undefined) {
|
|
6838
7009
|
throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
|
|
6839
7010
|
}
|
|
6840
|
-
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, blend, false, false, depthWrite, layer, true);
|
|
7011
|
+
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided);
|
|
6841
7012
|
}
|
|
6842
7013
|
const material = this.materialSlotForDraw();
|
|
6843
7014
|
if (material !== null) {
|
|
@@ -6978,7 +7149,10 @@ export class WebGPURenderer {
|
|
|
6978
7149
|
i[atF('uFogMode')] = medium.fogMode;
|
|
6979
7150
|
f[atF('uFogNear')] = medium.fogNear;
|
|
6980
7151
|
f[atF('uFogFar')] = medium.fogFar;
|
|
6981
|
-
this.
|
|
7152
|
+
const fragmentSlot = this.scatterFrags.allocate();
|
|
7153
|
+
if (fragmentSlot === null)
|
|
7154
|
+
return;
|
|
7155
|
+
this.scatterFrags.writeBlock(fragmentSlot, this.scatterFragInts);
|
|
6982
7156
|
const scatterPipe = scatterPipeline(this.pipelines, this.surface.device, this.scatterLayout);
|
|
6983
7157
|
/* One dynamic offset, and a group of two uniform buffers: nothing sampled, nothing read. */
|
|
6984
7158
|
const scatterCommand = this.recordDraw(0, this.currentTarget());
|
|
@@ -6986,7 +7160,8 @@ export class WebGPURenderer {
|
|
|
6986
7160
|
scatterCommand.pipeline = scatterPipe;
|
|
6987
7161
|
scatterCommand.bindGroup = this.scatterBindGroup;
|
|
6988
7162
|
scatterCommand.offsetA = slot;
|
|
6989
|
-
scatterCommand.
|
|
7163
|
+
scatterCommand.offsetB = fragmentSlot;
|
|
7164
|
+
scatterCommand.offsetCount = 2;
|
|
6990
7165
|
for (let index = 0; index < scatter.vertexBuffers.length; index++) {
|
|
6991
7166
|
scatterCommand.vertexBuffers[index] = scatter.vertexBuffers[index];
|
|
6992
7167
|
}
|
|
@@ -7001,7 +7176,7 @@ export class WebGPURenderer {
|
|
|
7001
7176
|
if (pass === null)
|
|
7002
7177
|
return;
|
|
7003
7178
|
pass.setPipeline(scatterPipe);
|
|
7004
|
-
pass.setBindGroup(0, this.scatterBindGroup, [slot]);
|
|
7179
|
+
pass.setBindGroup(0, this.scatterBindGroup, [slot, fragmentSlot]);
|
|
7005
7180
|
for (let index = 0; index < scatter.vertexBuffers.length; index++) {
|
|
7006
7181
|
pass.setVertexBuffer(index, scatter.vertexBuffers[index]);
|
|
7007
7182
|
}
|
|
@@ -7556,6 +7731,7 @@ export class WebGPURenderer {
|
|
|
7556
7731
|
*/
|
|
7557
7732
|
rebuildFlatGroupsForNewRings() {
|
|
7558
7733
|
this.flatBindGroups.clear();
|
|
7734
|
+
this.flatEnvironment = this.flatTextures('uEnvironment').view;
|
|
7559
7735
|
this.skinnedGroups.length = 0;
|
|
7560
7736
|
const albedo = this.albedo;
|
|
7561
7737
|
const normalMap = this.normalMap;
|
|
@@ -7574,6 +7750,25 @@ export class WebGPURenderer {
|
|
|
7574
7750
|
/* The open material lives in a slot of a buffer that no longer exists. */
|
|
7575
7751
|
this.materials.dirty();
|
|
7576
7752
|
}
|
|
7753
|
+
/**
|
|
7754
|
+
* The environment view the cached flat groups bind: the grid, or the blank stand-in while the
|
|
7755
|
+
* grid may not be read. What `rebindEnvironment` compares against.
|
|
7756
|
+
*/
|
|
7757
|
+
flatEnvironment = null;
|
|
7758
|
+
/**
|
|
7759
|
+
* Rebuild the flat groups if, and only if, the environment they bind has changed.
|
|
7760
|
+
*
|
|
7761
|
+
* **Every cached group holds the environment view**, so a change of it makes all of them stale;
|
|
7762
|
+
* but nothing else a bake does touches a group. Rebuilding on every bake's two edges regardless
|
|
7763
|
+
* emptied the cache twice a bake, and every material built its group again inside the bake and
|
|
7764
|
+
* again after it: a courtyard re-baking a few faces a frame spent 28% of its CPU there on the
|
|
7765
|
+
* native host, where a `createBindGroup` costs most. What would make this wrong is a second
|
|
7766
|
+
* binding that a bake changes, which would have to be compared here too.
|
|
7767
|
+
*/
|
|
7768
|
+
rebindEnvironment() {
|
|
7769
|
+
if (this.flatTextures('uEnvironment').view !== this.flatEnvironment)
|
|
7770
|
+
this.rebuildFlatBindGroup();
|
|
7771
|
+
}
|
|
7577
7772
|
/**
|
|
7578
7773
|
* Rebuild the group for the open material, and drop every cached one.
|
|
7579
7774
|
*
|
|
@@ -7585,6 +7780,7 @@ export class WebGPURenderer {
|
|
|
7585
7780
|
*/
|
|
7586
7781
|
rebuildFlatBindGroup() {
|
|
7587
7782
|
this.flatBindGroups.clear();
|
|
7783
|
+
this.flatEnvironment = this.flatTextures('uEnvironment').view;
|
|
7588
7784
|
if (this.albedo === null &&
|
|
7589
7785
|
this.normalMap === null &&
|
|
7590
7786
|
this.ormMap === null &&
|
|
@@ -7618,6 +7814,7 @@ export class WebGPURenderer {
|
|
|
7618
7814
|
drawSky(camera, sky, env) {
|
|
7619
7815
|
if (!this.canDraw())
|
|
7620
7816
|
return;
|
|
7817
|
+
this.frameSky = sky;
|
|
7621
7818
|
const f = this.skyFloats;
|
|
7622
7819
|
const at = (name) => SKY_FIELDS[name].offset / 4;
|
|
7623
7820
|
/*
|
|
@@ -7643,13 +7840,23 @@ export class WebGPURenderer {
|
|
|
7643
7840
|
const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
|
|
7644
7841
|
f.set(medium.underwaterColor, at('uUnderwaterColor'));
|
|
7645
7842
|
f[at('uUnderwaterFactor')] = medium.underwaterFactor;
|
|
7646
|
-
this.
|
|
7843
|
+
const slot = this.skyUniforms.allocate();
|
|
7844
|
+
if (slot === null) {
|
|
7845
|
+
if (!this.warnedSkyFull) {
|
|
7846
|
+
this.warnedSkyFull = true;
|
|
7847
|
+
console.warn(`WebGPU: more than ${MAX_SKY_DRAWS} sky draws in a frame; the rest are skipped`);
|
|
7848
|
+
}
|
|
7849
|
+
return;
|
|
7850
|
+
}
|
|
7851
|
+
this.skyUniforms.writeFloats(slot, 0, f);
|
|
7647
7852
|
const skyPipe = skyPipeline(this.pipelines, this.surface.device, this.skyLayout);
|
|
7648
7853
|
/* Reads nothing: the group is one uniform buffer, and the sky samples no attachment. */
|
|
7649
7854
|
const skyCommand = this.recordDraw(0, this.currentTarget());
|
|
7650
7855
|
if (skyCommand !== null) {
|
|
7651
7856
|
skyCommand.pipeline = skyPipe;
|
|
7652
7857
|
skyCommand.bindGroup = this.skyBindGroup;
|
|
7858
|
+
skyCommand.offsetA = slot;
|
|
7859
|
+
skyCommand.offsetCount = 1;
|
|
7653
7860
|
skyCommand.count = 3;
|
|
7654
7861
|
}
|
|
7655
7862
|
else {
|
|
@@ -7657,7 +7864,7 @@ export class WebGPURenderer {
|
|
|
7657
7864
|
if (pass === null)
|
|
7658
7865
|
return;
|
|
7659
7866
|
pass.setPipeline(skyPipe);
|
|
7660
|
-
pass.setBindGroup(0, this.skyBindGroup);
|
|
7867
|
+
pass.setBindGroup(0, this.skyBindGroup, [slot]);
|
|
7661
7868
|
pass.draw(3);
|
|
7662
7869
|
}
|
|
7663
7870
|
}
|
|
@@ -7769,6 +7976,18 @@ export class WebGPURenderer {
|
|
|
7769
7976
|
const bloomStrength = this.quality.bloom * this.bloomScale;
|
|
7770
7977
|
if (bloomStrength > 0)
|
|
7771
7978
|
this.runBloom(encoder);
|
|
7979
|
+
/*
|
|
7980
|
+
* Eye adaptation, metered from the scene the composite shows, and local exposure's grid beside
|
|
7981
|
+
* it. The held brightness is kept for either, since local exposure moves regions relative to it.
|
|
7982
|
+
* See `setAutoExposure` and `setLocalExposure`.
|
|
7983
|
+
*/
|
|
7984
|
+
const localising = this.localExposure > 0;
|
|
7985
|
+
const measuring = (this.autoExposure > 0 || localising) && this.exposurePass !== null;
|
|
7986
|
+
const adapting = measuring && this.autoExposure > 0;
|
|
7987
|
+
const shownScene = this.reconstructing ? this.reconShownView : this.sceneColorView;
|
|
7988
|
+
if (measuring && shownScene !== null) {
|
|
7989
|
+
this.exposurePass?.run(encoder, shownScene, this.autoExposureDt, localising);
|
|
7990
|
+
}
|
|
7772
7991
|
/* The grade travels here only where the mesh pass gave it up. See `bindMeshPass`. */
|
|
7773
7992
|
const graded = this.quality.hdrScene;
|
|
7774
7993
|
const f = this.rushFloats;
|
|
@@ -7807,6 +8026,12 @@ export class WebGPURenderer {
|
|
|
7807
8026
|
/* The grade, sampled after the curve and before the veil. See `uGradeLut` in `rush.ts`. */
|
|
7808
8027
|
f[at('uGradeStrength')] = this.gradeStrength;
|
|
7809
8028
|
f[at('uGradeSize')] = this.gradeSize;
|
|
8029
|
+
/* The lens and the print. See `setVignette` and `setFilmGrain`. */
|
|
8030
|
+
f[at('uVignette')] = this.vignette;
|
|
8031
|
+
f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
|
|
8032
|
+
f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
|
|
8033
|
+
f[at('uGrain')] = this.grain;
|
|
8034
|
+
f[at('uGrain') + 1] = this.grainSeed;
|
|
7810
8035
|
device.queue.writeBuffer(this.rushUniforms, 0, this.rushStaging);
|
|
7811
8036
|
const pass = encoder.beginRenderPass({
|
|
7812
8037
|
label: 'post.composite',
|
|
@@ -7877,6 +8102,10 @@ export class WebGPURenderer {
|
|
|
7877
8102
|
a frame that never grades anything should not carry a texture, and a declared binding
|
|
7878
8103
|
must still have a complete one — the argument the bloom fallback above makes. */
|
|
7879
8104
|
...entry(RUSH_TEXTURES.uGradeLut, this.ensureGradeView()),
|
|
8105
|
+
/* The scene as the placeholder until adaptation is asked for: the strength is zero then,
|
|
8106
|
+
and a declared binding still needs a complete texture, as the bloom fallback says. */
|
|
8107
|
+
...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
|
|
8108
|
+
...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
|
|
7880
8109
|
],
|
|
7881
8110
|
layout: this.rushLayout,
|
|
7882
8111
|
});
|
|
@@ -8896,12 +9125,15 @@ export class WebGPURenderer {
|
|
|
8896
9125
|
otherwise halve the blur and leave half the pattern standing. */
|
|
8897
9126
|
const width = Math.max(1, this.aoTarget?.width ?? 1);
|
|
8898
9127
|
const height = Math.max(1, this.aoTarget?.height ?? 1);
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
9128
|
+
/* Across at the estimate's own scale, which the second axis needs whole; down decoded, so the
|
|
9129
|
+
composite reads open sky as one. The same two `ambientOcclusionPass.ts` writes. */
|
|
9130
|
+
for (const [index, step, scale] of [
|
|
9131
|
+
[0, [1 / width, 0], 1],
|
|
9132
|
+
[1, [0, 1 / height], 1 / AO_STORE],
|
|
8902
9133
|
]) {
|
|
8903
9134
|
f[slot(index) + at('uStep')] = step[0];
|
|
8904
9135
|
f[slot(index) + at('uStep') + 1] = step[1];
|
|
9136
|
+
f[slot(index) + at('uAoScale')] = scale;
|
|
8905
9137
|
f[slot(index) + at('uDepthToViewZ')] = inv[10] ?? 0;
|
|
8906
9138
|
f[slot(index) + at('uDepthToViewZ') + 1] = inv[14] ?? 0;
|
|
8907
9139
|
f[slot(index) + at('uDepthToViewZ') + 2] = inv[11] ?? 0;
|
|
@@ -9586,8 +9818,13 @@ export class WebGPURenderer {
|
|
|
9586
9818
|
const swap = this.swapView();
|
|
9587
9819
|
if (swap === null)
|
|
9588
9820
|
return null;
|
|
9589
|
-
|
|
9590
|
-
|
|
9821
|
+
/*
|
|
9822
|
+
* **The drawing buffer's size, not the scene depth's.** The overlay draws on the swap image,
|
|
9823
|
+
* past the resolve, and a reconstruction draws the scene depth small: sized from that, the
|
|
9824
|
+
* device refused the pass beside a full-size swap image and every overlay went missing.
|
|
9825
|
+
*/
|
|
9826
|
+
const width = Math.max(1, this.surface.canvas.width);
|
|
9827
|
+
const height = Math.max(1, this.surface.canvas.height);
|
|
9591
9828
|
if (this.overlayDepth === null ||
|
|
9592
9829
|
this.overlayDepth.width !== width ||
|
|
9593
9830
|
this.overlayDepth.height !== height) {
|
|
@@ -9926,10 +10163,26 @@ export class WebGPURenderer {
|
|
|
9926
10163
|
* had stopped declaring. What the guard is actually for is not *building* a composer for a
|
|
9927
10164
|
* profile that never declares anything, so that is all it now guards.
|
|
9928
10165
|
*/
|
|
10166
|
+
if (this.fieldComposer === null && this.distanceFields.length === 0)
|
|
10167
|
+
return;
|
|
10168
|
+
/*
|
|
10169
|
+
* **As many cascades as reach the probe grid**, and rebuilt when the grid asks for a different
|
|
10170
|
+
* number: the far end of a courtyard longer than the default 16 m was traced against no field
|
|
10171
|
+
* at all and lit as open sky from every side. See `gi/fieldReach.ts`.
|
|
10172
|
+
*/
|
|
10173
|
+
const grid = this.indirectGrid();
|
|
10174
|
+
const cascades = grid === null
|
|
10175
|
+
? DEFAULT_FIELD_COMPOSE.cascades
|
|
10176
|
+
: cascadesForGrid(grid, DEFAULT_FIELD_COMPOSE.radius, DEFAULT_FIELD_COMPOSE.cascades, MAX_FIELD_CASCADES);
|
|
10177
|
+
if (this.fieldComposer !== null && this.fieldComposer.cascadeCount !== cascades) {
|
|
10178
|
+
this.fieldComposer.dispose();
|
|
10179
|
+
this.fieldComposer = null;
|
|
10180
|
+
}
|
|
9929
10181
|
if (this.fieldComposer === null) {
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
10182
|
+
this.fieldComposer = new FieldComposer(this.surface.device, {
|
|
10183
|
+
...DEFAULT_FIELD_COMPOSE,
|
|
10184
|
+
cascades,
|
|
10185
|
+
});
|
|
9933
10186
|
}
|
|
9934
10187
|
this.fieldComposer.compose(encoder, this.distanceFields, this.frameEye);
|
|
9935
10188
|
}
|
|
@@ -9996,11 +10249,20 @@ export class WebGPURenderer {
|
|
|
9996
10249
|
* never said what was lighting it.
|
|
9997
10250
|
*/
|
|
9998
10251
|
const env = this.frameEnv;
|
|
9999
|
-
|
|
10252
|
+
/*
|
|
10253
|
+
* And the scene's summed fixed lights, which the trace adds to what a hit receives: the bounce
|
|
10254
|
+
* of every candle, not only of the ones near enough to be shaded exactly.
|
|
10255
|
+
*/
|
|
10256
|
+
resolveDriftLight(this.bakeLightField === null ? null : this.lightField, true, this.frameEye, this.bakeDriftUniforms);
|
|
10257
|
+
/* And the frame's exact lights, flickering and moving as the frame shades them. */
|
|
10258
|
+
this.bakeLamps.count = resolveBounceLights(env, this.bounceLights);
|
|
10259
|
+
this.bakeLamps.falloff = this.lights.falloff;
|
|
10260
|
+
this.probeBaker.bake(encoder, field, grid, this.probeBaked, this.bakeLightField, this.bakeLamps, {
|
|
10000
10261
|
direction: env?.directionalDir ?? NO_SUN,
|
|
10001
10262
|
colour: env?.directionalColor ?? NO_SUN,
|
|
10002
|
-
/* What a ray that left the world finds
|
|
10003
|
-
|
|
10263
|
+
/* What a ray that left the world finds: the sky this frame drew, or its ambient. */
|
|
10264
|
+
ambient: env?.ambient ?? NO_SUN,
|
|
10265
|
+
sky: this.frameSky,
|
|
10004
10266
|
});
|
|
10005
10267
|
/*
|
|
10006
10268
|
* **The bake is what fills the layers now, so it is what marks them filled.** `probeBaked`
|
|
@@ -10191,7 +10453,7 @@ export class WebGPURenderer {
|
|
|
10191
10453
|
* currently open. Offering it earlier hands a pass a texture of undefined contents, which is
|
|
10192
10454
|
* the failure `probeBaked` was introduced for on the forward path.
|
|
10193
10455
|
*/
|
|
10194
|
-
const usable = this.probeView !== null && this.
|
|
10456
|
+
const usable = this.probeView !== null && this.probeReadable();
|
|
10195
10457
|
if (usable) {
|
|
10196
10458
|
const environment = this.passEnvironment;
|
|
10197
10459
|
environment.view = this.probeView;
|
|
@@ -10631,6 +10893,8 @@ export class WebGPURenderer {
|
|
|
10631
10893
|
* -----------------------------------------------------------------------------------------
|
|
10632
10894
|
*/
|
|
10633
10895
|
bloomScale = 1;
|
|
10896
|
+
/** In scene units, once `setBloom` has moved it; null reads the profile's. */
|
|
10897
|
+
bloomThresholdSet = null;
|
|
10634
10898
|
rushStrength = 0;
|
|
10635
10899
|
motionBlurScale = 1;
|
|
10636
10900
|
/**
|
|
@@ -10642,6 +10906,48 @@ export class WebGPURenderer {
|
|
|
10642
10906
|
*/
|
|
10643
10907
|
veilColor = [0, 0, 0];
|
|
10644
10908
|
veilAlpha = 0;
|
|
10909
|
+
vignette = 0;
|
|
10910
|
+
grain = 0;
|
|
10911
|
+
grainSeed = 0;
|
|
10912
|
+
warnedLookWithoutComposite = false;
|
|
10913
|
+
/** Eye adaptation: how far, this frame's time step, and the pass once asked for. */
|
|
10914
|
+
autoExposure = 0;
|
|
10915
|
+
autoExposureDt = 0;
|
|
10916
|
+
/** How far each region is brought toward the frame. See `setLocalExposure`. */
|
|
10917
|
+
localExposure = 0;
|
|
10918
|
+
exposurePass = null;
|
|
10919
|
+
/**
|
|
10920
|
+
* How strongly the lens darkens the frame's corners, held until changed: 0 is none, 0.5 is about
|
|
10921
|
+
* 1.2 stops at the corner. Applied to scene light before the tone curve, as a lens loses light;
|
|
10922
|
+
* see `filmLook.ts`. Needs `screenEffects`, and says so once rather than doing nothing.
|
|
10923
|
+
*/
|
|
10924
|
+
setVignette(strength) {
|
|
10925
|
+
if (!this.hasComposite) {
|
|
10926
|
+
this.warnLookWithoutComposite(strength);
|
|
10927
|
+
return;
|
|
10928
|
+
}
|
|
10929
|
+
this.vignette = clampVignette(strength);
|
|
10930
|
+
}
|
|
10931
|
+
/**
|
|
10932
|
+
* Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
|
|
10933
|
+
* grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
|
|
10934
|
+
* its caller: pass a new one each frame for grain that moves, and the same one for a still that
|
|
10935
|
+
* is identical run to run. Applied last, after the grade and the veil; see `filmLook.ts`.
|
|
10936
|
+
*/
|
|
10937
|
+
setFilmGrain(strength, seed) {
|
|
10938
|
+
if (!this.hasComposite) {
|
|
10939
|
+
this.warnLookWithoutComposite(strength);
|
|
10940
|
+
return;
|
|
10941
|
+
}
|
|
10942
|
+
this.grain = clampGrain(strength);
|
|
10943
|
+
this.grainSeed = grainSeed(seed);
|
|
10944
|
+
}
|
|
10945
|
+
warnLookWithoutComposite(strength) {
|
|
10946
|
+
if (strength > 0 && !this.warnedLookWithoutComposite) {
|
|
10947
|
+
this.warnedLookWithoutComposite = true;
|
|
10948
|
+
console.warn(FILM_LOOK_WITHOUT_COMPOSITE);
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10645
10951
|
/*
|
|
10646
10952
|
* **These three warned that the composite did not exist, and went on warning after it did.**
|
|
10647
10953
|
*
|
|
@@ -10717,8 +11023,17 @@ export class WebGPURenderer {
|
|
|
10717
11023
|
this.gradeView = this.gradeTexture.createView({ dimension: '3d' });
|
|
10718
11024
|
this.gradeSize = lut.size;
|
|
10719
11025
|
}
|
|
10720
|
-
setBloom(scale) {
|
|
11026
|
+
setBloom(scale, threshold) {
|
|
10721
11027
|
this.bloomScale = Math.min(Math.max(scale, 0), 1);
|
|
11028
|
+
if (threshold === undefined)
|
|
11029
|
+
return;
|
|
11030
|
+
const next = bloomThresholdOf(threshold);
|
|
11031
|
+
if (next === this.bloomThresholdSet)
|
|
11032
|
+
return;
|
|
11033
|
+
this.bloomThresholdSet = next;
|
|
11034
|
+
/* The blocks are written once per size, so a moved threshold rewrites them now. */
|
|
11035
|
+
if (this.bloomLevels.length > 0)
|
|
11036
|
+
this.writeBloomBlocks();
|
|
10722
11037
|
}
|
|
10723
11038
|
/**
|
|
10724
11039
|
* How thick the air is, this frame: a global participating medium filling the whole frustum.
|
|
@@ -10743,6 +11058,62 @@ export class WebGPURenderer {
|
|
|
10743
11058
|
setCameraMotionBlur(scale) {
|
|
10744
11059
|
this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
|
|
10745
11060
|
}
|
|
11061
|
+
/**
|
|
11062
|
+
* The camera has cut: this frame is a new shot, not the next moment of the last one.
|
|
11063
|
+
*
|
|
11064
|
+
* **Everything temporal reprojects through the previous frame's view**: the motion blur measures
|
|
11065
|
+
* its smear against it, and the temporal resolve reads its history through it. A cut is a jump
|
|
11066
|
+
* the renderer cannot tell from a fast camera, so without this a transport that seeks, a respawn
|
|
11067
|
+
* or an edit smears its first frame along the whole jump and blends in a picture of somewhere
|
|
11068
|
+
* else. The caller knows it cut; this is how it says so, before the frame's `beginFrame`.
|
|
11069
|
+
*
|
|
11070
|
+
* What it gives up is one frame of history: the frame after a cut has no blur and no temporal
|
|
11071
|
+
* blend, as the first frame of a session has none. Nothing else is reset.
|
|
11072
|
+
*/
|
|
11073
|
+
cameraCut() {
|
|
11074
|
+
this.hasPreviousView = false;
|
|
11075
|
+
this.temporalHistory.invalidate();
|
|
11076
|
+
/* The reconstruction keeps histories of its own, and they are the last shot's too. */
|
|
11077
|
+
this.reconHasHistory = false;
|
|
11078
|
+
/* And the eye was adapted to the last shot. */
|
|
11079
|
+
this.exposurePass?.cut();
|
|
11080
|
+
}
|
|
11081
|
+
/**
|
|
11082
|
+
* Eye adaptation. **The other backend's `setAutoExposure` carries the reasoning**, whole; this is
|
|
11083
|
+
* the same setter over the same state, and the pass is built the first time it is asked for, so
|
|
11084
|
+
* a renderer that never adapts carries none of it.
|
|
11085
|
+
*/
|
|
11086
|
+
setAutoExposure(strength, dtSec) {
|
|
11087
|
+
if (!this.hasComposite || !this.quality.hdrScene) {
|
|
11088
|
+
this.warnLookWithoutComposite(strength);
|
|
11089
|
+
return;
|
|
11090
|
+
}
|
|
11091
|
+
this.autoExposure = clampAutoExposure(strength);
|
|
11092
|
+
this.autoExposureDt = dtSec;
|
|
11093
|
+
if (this.autoExposure > 0)
|
|
11094
|
+
this.ensureExposurePass();
|
|
11095
|
+
}
|
|
11096
|
+
/**
|
|
11097
|
+
* Local exposure. **The other backend's `setLocalExposure` carries the reasoning**, whole; this is
|
|
11098
|
+
* the same setter over the same state, sharing eye adaptation's pass, which is built the first
|
|
11099
|
+
* time either is asked for.
|
|
11100
|
+
*/
|
|
11101
|
+
setLocalExposure(strength) {
|
|
11102
|
+
if (!this.hasComposite || !this.quality.hdrScene) {
|
|
11103
|
+
this.warnLookWithoutComposite(strength);
|
|
11104
|
+
return;
|
|
11105
|
+
}
|
|
11106
|
+
this.localExposure = clampLocalExposure(strength);
|
|
11107
|
+
if (this.localExposure > 0)
|
|
11108
|
+
this.ensureExposurePass();
|
|
11109
|
+
}
|
|
11110
|
+
/** The exposure pass, built once; the composite's group holds placeholders until it exists. */
|
|
11111
|
+
ensureExposurePass() {
|
|
11112
|
+
if (this.exposurePass !== null)
|
|
11113
|
+
return;
|
|
11114
|
+
this.exposurePass = new ExposurePass(this.surface.device, this.pipelines, this.postSampler);
|
|
11115
|
+
this.rebuildRushBindGroup();
|
|
11116
|
+
}
|
|
10746
11117
|
/**
|
|
10747
11118
|
* Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
|
|
10748
11119
|
* to take. **The other backend's `setDepthOfField` carries the reasoning**, whole: the
|
|
@@ -10947,6 +11318,8 @@ export class WebGPURenderer {
|
|
|
10947
11318
|
* over a lobe-convolved one.
|
|
10948
11319
|
*/
|
|
10949
11320
|
probe;
|
|
11321
|
+
/** The views, groups and attachments a bake draws with, made once. See `probeTargets.ts`. */
|
|
11322
|
+
probeTargets;
|
|
10950
11323
|
/**
|
|
10951
11324
|
* The prefiltered cube: what the lit pass samples, one GGX-convolved level per roughness.
|
|
10952
11325
|
*
|
|
@@ -10978,6 +11351,19 @@ export class WebGPURenderer {
|
|
|
10978
11351
|
probeMipLayout;
|
|
10979
11352
|
/** The photometric atlas, allocated on first use. See `setIesProfiles`. */
|
|
10980
11353
|
iesTexture = null;
|
|
11354
|
+
/** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
|
|
11355
|
+
lightField = null;
|
|
11356
|
+
driftIndex = null;
|
|
11357
|
+
driftAtlas = null;
|
|
11358
|
+
driftIndexView = null;
|
|
11359
|
+
driftAtlasView = null;
|
|
11360
|
+
driftUniforms = createDriftLightUniforms();
|
|
11361
|
+
/** The same field resolved for a traced probe, and what the baker binds; null while not whole. */
|
|
11362
|
+
bakeDriftUniforms = createDriftLightUniforms();
|
|
11363
|
+
bakeLightField = null;
|
|
11364
|
+
/** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
|
|
11365
|
+
bounceLights = createBounceLights();
|
|
11366
|
+
bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
|
|
10981
11367
|
iesView = null;
|
|
10982
11368
|
cookieTexture = null;
|
|
10983
11369
|
cookieView = null;
|
|
@@ -11002,8 +11388,9 @@ export class WebGPURenderer {
|
|
|
11002
11388
|
* mirroring uninitialised memory is worse than a car mirroring a gradient — the rule one probe
|
|
11003
11389
|
* already had, unchanged by there being more of them.
|
|
11004
11390
|
*/
|
|
11005
|
-
|
|
11006
|
-
|
|
11391
|
+
probeSweeps = null;
|
|
11392
|
+
/** `uProbeGridSets`, rewritten in place. */
|
|
11393
|
+
probeSets = new Float32Array(3);
|
|
11007
11394
|
/**
|
|
11008
11395
|
* Whether a bake is running, which is when the cube may be neither sampled nor bound.
|
|
11009
11396
|
*
|
|
@@ -11011,13 +11398,23 @@ export class WebGPURenderer {
|
|
|
11011
11398
|
* load-bearing rather than defensive: WebGPU rejects a pass that holds the texture it writes.
|
|
11012
11399
|
*/
|
|
11013
11400
|
probePassActive = false;
|
|
11401
|
+
/** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
|
|
11402
|
+
probeBounce = false;
|
|
11403
|
+
/**
|
|
11404
|
+
* Whether the shading may read the grid now: once every probe is filled, and not during a bake
|
|
11405
|
+
* unless it is a bounce bake. A bounce bake's faces are drawn into the probe's own cube and only
|
|
11406
|
+
* the resolve writes the array, in a pass of its own, so no pass both samples and writes it.
|
|
11407
|
+
*/
|
|
11408
|
+
probeReadable() {
|
|
11409
|
+
return this.probeBaked && (!this.probePassActive || this.probeBounce);
|
|
11410
|
+
}
|
|
11014
11411
|
/** How many mip levels the roughness blur has to work with. `flat.ts` reads this. */
|
|
11015
11412
|
get probeMaxLod() {
|
|
11016
11413
|
return ggxMaxLevelFor(this.probeEdge);
|
|
11017
11414
|
}
|
|
11018
|
-
/** Whether
|
|
11415
|
+
/** Whether a whole sweep of the grid has been convolved. See `probeSweeps.ts`. */
|
|
11019
11416
|
get probeBaked() {
|
|
11020
|
-
return this.probes !== null && this.
|
|
11417
|
+
return this.probes !== null && this.probeSweeps?.ready === true;
|
|
11021
11418
|
}
|
|
11022
11419
|
/**
|
|
11023
11420
|
* Record that one layer now holds a convolution, without counting a rebake twice.
|
|
@@ -11036,13 +11433,9 @@ export class WebGPURenderer {
|
|
|
11036
11433
|
* mistake, and `setProbeGrid` says it about the array being replaced.
|
|
11037
11434
|
*/
|
|
11038
11435
|
markProbeFilled(layer) {
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
if (this.
|
|
11042
|
-
return;
|
|
11043
|
-
this.probeFilled[layer] = 1;
|
|
11044
|
-
this.probeFilledCount++;
|
|
11045
|
-
if (this.probeBaked)
|
|
11436
|
+
const was = this.probeBaked;
|
|
11437
|
+
this.probeSweeps?.baked(layer);
|
|
11438
|
+
if (!was && this.probeBaked)
|
|
11046
11439
|
this.rebuildFlatGroupsForNewRings();
|
|
11047
11440
|
}
|
|
11048
11441
|
/**
|
|
@@ -11053,6 +11446,7 @@ export class WebGPURenderer {
|
|
|
11053
11446
|
*/
|
|
11054
11447
|
allocateProbeArray(layers, device) {
|
|
11055
11448
|
this.probeArray?.destroy();
|
|
11449
|
+
this.probeTargets?.forgetArray();
|
|
11056
11450
|
this.probeArray = device.createTexture({
|
|
11057
11451
|
label: 'probe.array',
|
|
11058
11452
|
size: [this.probeEdge, this.probeEdge, layers],
|
|
@@ -11062,8 +11456,6 @@ export class WebGPURenderer {
|
|
|
11062
11456
|
usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
|
|
11063
11457
|
});
|
|
11064
11458
|
this.probeView = this.probeArray.createView({ dimension: '2d-array' });
|
|
11065
|
-
this.probeFilled = new Uint8Array(layers);
|
|
11066
|
-
this.probeFilledCount = 0;
|
|
11067
11459
|
/*
|
|
11068
11460
|
* **Cleared here, because a texture no pass has written holds undefined contents** — the same
|
|
11069
11461
|
* sentence the directional shadow maps are cleared under, arrived at from the other end.
|
|
@@ -11120,8 +11512,14 @@ export class WebGPURenderer {
|
|
|
11120
11512
|
if (current !== null && this.probeArray !== null && sameGrid(current, grid))
|
|
11121
11513
|
return true;
|
|
11122
11514
|
this.probes = grid;
|
|
11123
|
-
|
|
11124
|
-
|
|
11515
|
+
/* A traced grid writes its layers itself, so it keeps one set. See `ProbeGridOptions.crossfade`. */
|
|
11516
|
+
const crossfade = grid.crossfade && !this.quality.indirectLight;
|
|
11517
|
+
const sets = crossfade ? 3 : 1;
|
|
11518
|
+
if (this.probeArray === null ||
|
|
11519
|
+
this.probeArray.depthOrArrayLayers !== grid.layers * sets ||
|
|
11520
|
+
this.probeSweeps?.sets !== sets) {
|
|
11521
|
+
this.probeSweeps = new ProbeSweeps(grid.layers, crossfade);
|
|
11522
|
+
this.allocateProbeArray(grid.layers * sets, this.surface.device);
|
|
11125
11523
|
this.rebuildFlatGroupsForNewRings();
|
|
11126
11524
|
}
|
|
11127
11525
|
return true;
|
|
@@ -11183,39 +11581,18 @@ export class WebGPURenderer {
|
|
|
11183
11581
|
camera.far = PROBE_FAR_M;
|
|
11184
11582
|
camera.roll = 0;
|
|
11185
11583
|
const { device } = this.surface;
|
|
11186
|
-
const
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
format: DEPTH_FORMAT,
|
|
11190
|
-
/*
|
|
11191
|
-
* **The world's sample count, because a bake draws the world with the world's pipelines.**
|
|
11192
|
-
* A pipeline whose count disagrees with its attachment is rejected at `finish` and takes
|
|
11193
|
-
* the whole bake with it, and WebGPU requires every attachment in a pass to agree — so a
|
|
11194
|
-
* single-sample depth beside a four-sample colour is refused in words that read as a depth
|
|
11195
|
-
* problem and are a multisampling one.
|
|
11196
|
-
*/
|
|
11197
|
-
sampleCount: this.samples,
|
|
11198
|
-
usage: 0x10, // RENDER_ATTACHMENT
|
|
11199
|
-
});
|
|
11200
|
-
const depthView = depth.createView();
|
|
11584
|
+
const targets = this.probeTargets;
|
|
11585
|
+
if (targets === null)
|
|
11586
|
+
return false;
|
|
11201
11587
|
/*
|
|
11202
|
-
* The
|
|
11203
|
-
*
|
|
11204
|
-
*
|
|
11205
|
-
*
|
|
11206
|
-
*
|
|
11207
|
-
*
|
|
11588
|
+
* The depth, at the world's sample count because a bake draws the world with the world's
|
|
11589
|
+
* pipelines: a pipeline whose count disagrees with its attachment is rejected at `finish` and
|
|
11590
|
+
* takes the whole bake with it, in words that read as a depth problem and are a multisampling
|
|
11591
|
+
* one. And the multisampled twin the faces are drawn into and resolved from, since **a cube
|
|
11592
|
+
* face cannot be both multisampled and sampled** — the same bind the mirror is in. Both kept
|
|
11593
|
+
* between bakes: see `probeTargets.ts`.
|
|
11208
11594
|
*/
|
|
11209
|
-
const colorMsaa = this.samples
|
|
11210
|
-
? device.createTexture({
|
|
11211
|
-
label: 'probe.colorMsaa',
|
|
11212
|
-
size: [this.probeSize, this.probeSize],
|
|
11213
|
-
format: this.pipelines.format,
|
|
11214
|
-
sampleCount: this.samples,
|
|
11215
|
-
usage: 0x10, // RENDER_ATTACHMENT
|
|
11216
|
-
})
|
|
11217
|
-
: null;
|
|
11218
|
-
const colorMsaaView = colorMsaa?.createView() ?? null;
|
|
11595
|
+
const { depth: depthView, colorMsaa: colorMsaaView } = targets.attachments(this.samples, this.pipelines.format);
|
|
11219
11596
|
/*
|
|
11220
11597
|
* The frame's own pass is set aside and restored, because `drawFace` calls straight back into
|
|
11221
11598
|
* `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
|
|
@@ -11234,16 +11611,26 @@ export class WebGPURenderer {
|
|
|
11234
11611
|
if (this.quality.frameGraph)
|
|
11235
11612
|
this.flushGraph();
|
|
11236
11613
|
const savedEncoder = this.encoder;
|
|
11614
|
+
/*
|
|
11615
|
+
* **The rings are marked here and given back after the submit below**, because this bake is its
|
|
11616
|
+
* own encoder: once it is submitted nothing in the frame can read its slots, and a grid baked in
|
|
11617
|
+
* one call would otherwise spend the frame's whole ring on probes and skip the draws past it.
|
|
11618
|
+
*/
|
|
11619
|
+
this.markRings();
|
|
11620
|
+
const skinMark = this.skinPalettes.mark();
|
|
11621
|
+
const frameEpoch = this.encoderEpoch;
|
|
11622
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
11237
11623
|
const encoder = device.createCommandEncoder({ label: 'probe' });
|
|
11238
11624
|
this.encoder = encoder;
|
|
11239
11625
|
/*
|
|
11240
|
-
* Fenced before the first face and released after the last,
|
|
11241
|
-
*
|
|
11242
|
-
*
|
|
11243
|
-
*
|
|
11626
|
+
* Fenced before the first face and released after the last, so the grid is not bound while a
|
|
11627
|
+
* plain bake is drawn. **Rebuilt only where that changes what the group binds**: a bounce bake
|
|
11628
|
+
* of a whole grid binds it exactly as the frame does, and it is the bake a scene repeats every
|
|
11629
|
+
* frame, a few faces at a time. See `rebindEnvironment`.
|
|
11244
11630
|
*/
|
|
11631
|
+
this.probeBounce = options?.bounce ?? false;
|
|
11245
11632
|
this.probePassActive = true;
|
|
11246
|
-
this.
|
|
11633
|
+
this.rebindEnvironment();
|
|
11247
11634
|
/*
|
|
11248
11635
|
* **A probe stores radiance, not display pixels.** See `renderer.ts` for the whole argument:
|
|
11249
11636
|
* baking through the output transform crushes every highlight with the tone curve before it
|
|
@@ -11255,7 +11642,9 @@ export class WebGPURenderer {
|
|
|
11255
11642
|
this.perFrameInts[this.materialField('uOutputTransform')] = 0;
|
|
11256
11643
|
this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
|
|
11257
11644
|
this.materials.dirty();
|
|
11258
|
-
|
|
11645
|
+
/* Only the faces asked for; the chain waits for the sixth. See `ProbeBakeOptions.faces`. */
|
|
11646
|
+
const [firstFace, endFace] = probeFaceRange(options);
|
|
11647
|
+
for (let face = firstFace; face < endFace; face++) {
|
|
11259
11648
|
const aim = PROBE_FACES[face];
|
|
11260
11649
|
if (aim === undefined)
|
|
11261
11650
|
continue;
|
|
@@ -11266,13 +11655,7 @@ export class WebGPURenderer {
|
|
|
11266
11655
|
* face and shows up as a reflection that slides at the wrong rate across a curved surface.
|
|
11267
11656
|
*/
|
|
11268
11657
|
camera.updateMatrices(1);
|
|
11269
|
-
const faceView =
|
|
11270
|
-
dimension: '2d',
|
|
11271
|
-
baseArrayLayer: face,
|
|
11272
|
-
arrayLayerCount: 1,
|
|
11273
|
-
baseMipLevel: 0,
|
|
11274
|
-
mipLevelCount: 1,
|
|
11275
|
-
});
|
|
11658
|
+
const faceView = targets.faceViews[face];
|
|
11276
11659
|
this.pass = encoder.beginRenderPass({
|
|
11277
11660
|
label: `probe.face${face}`,
|
|
11278
11661
|
colorAttachments: [
|
|
@@ -11318,19 +11701,23 @@ export class WebGPURenderer {
|
|
|
11318
11701
|
this.pass.end();
|
|
11319
11702
|
}
|
|
11320
11703
|
this.pass = null;
|
|
11321
|
-
|
|
11704
|
+
const whole = endFace === PROBE_FACES.length;
|
|
11705
|
+
if (whole)
|
|
11706
|
+
this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
|
|
11322
11707
|
this.flushRings();
|
|
11323
11708
|
device.queue.submit([encoder.finish()]);
|
|
11324
|
-
|
|
11325
|
-
|
|
11709
|
+
this.rewindRings(skinMark);
|
|
11710
|
+
this.encoderEpoch = frameEpoch;
|
|
11326
11711
|
this.encoder = savedEncoder;
|
|
11327
11712
|
this.pass = savedPass;
|
|
11328
|
-
|
|
11713
|
+
if (whole)
|
|
11714
|
+
this.markProbeFilled(layer);
|
|
11329
11715
|
this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
|
|
11330
11716
|
this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
|
|
11331
11717
|
this.materials.dirty();
|
|
11332
11718
|
this.probePassActive = false;
|
|
11333
|
-
this.
|
|
11719
|
+
this.probeBounce = false;
|
|
11720
|
+
this.rebindEnvironment();
|
|
11334
11721
|
return true;
|
|
11335
11722
|
}
|
|
11336
11723
|
/**
|
|
@@ -11457,6 +11844,77 @@ export class WebGPURenderer {
|
|
|
11457
11844
|
this.cookieTiles = tiles;
|
|
11458
11845
|
this.rebuildFlatBindGroup();
|
|
11459
11846
|
}
|
|
11847
|
+
/**
|
|
11848
|
+
* Sum a scene's many fixed lights into a DriftLight field. The same contract as
|
|
11849
|
+
* `Renderer.createLightField`; see `driftLight/lightField.ts`.
|
|
11850
|
+
*/
|
|
11851
|
+
createLightField(lights, options = {}) {
|
|
11852
|
+
this.forgetLightFieldVolumes();
|
|
11853
|
+
const field = new LightField(lights, { ...options, falloff: this.quality.pointLightFalloff });
|
|
11854
|
+
this.lightField = field;
|
|
11855
|
+
return field;
|
|
11856
|
+
}
|
|
11857
|
+
/** Let go of the field and its volumes; the scene's lights are all exact again. */
|
|
11858
|
+
disposeLightField() {
|
|
11859
|
+
this.forgetLightFieldVolumes();
|
|
11860
|
+
this.lightField = null;
|
|
11861
|
+
}
|
|
11862
|
+
forgetLightFieldVolumes() {
|
|
11863
|
+
const bound = this.driftAtlas !== null;
|
|
11864
|
+
this.driftIndex?.destroy();
|
|
11865
|
+
this.driftAtlas?.destroy();
|
|
11866
|
+
this.driftIndex = null;
|
|
11867
|
+
this.driftAtlas = null;
|
|
11868
|
+
this.driftIndexView = null;
|
|
11869
|
+
this.driftAtlasView = null;
|
|
11870
|
+
this.bakeLightField = null;
|
|
11871
|
+
/* The groups hold the destroyed volumes, and a group holding a destroyed texture fails. */
|
|
11872
|
+
if (bound && !this.surface.lost)
|
|
11873
|
+
this.rebuildFlatBindGroup();
|
|
11874
|
+
}
|
|
11875
|
+
/**
|
|
11876
|
+
* Upload the field's two volumes, once, when its last brick has baked, and rebuild the groups
|
|
11877
|
+
* that bind them: nothing samples them before that, so uploading as bricks land would be work
|
|
11878
|
+
* for no picture, and a rebuild per brick would be a bind group per brick.
|
|
11879
|
+
*/
|
|
11880
|
+
uploadLightField() {
|
|
11881
|
+
const field = this.lightField;
|
|
11882
|
+
if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
|
|
11883
|
+
return;
|
|
11884
|
+
}
|
|
11885
|
+
if (this.surface.lost)
|
|
11886
|
+
return;
|
|
11887
|
+
const { device } = this.surface;
|
|
11888
|
+
const [ix, iy, iz] = field.layout.dims;
|
|
11889
|
+
const index = device.createTexture({
|
|
11890
|
+
label: 'driftLight.index',
|
|
11891
|
+
size: [ix, iy, iz],
|
|
11892
|
+
dimension: '3d',
|
|
11893
|
+
format: 'r32uint',
|
|
11894
|
+
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
11895
|
+
});
|
|
11896
|
+
device.queue.writeTexture({ texture: index }, field.layout.index, { bytesPerRow: ix * 4, rowsPerImage: iy }, [ix, iy, iz]);
|
|
11897
|
+
const [aw, ah, ad] = field.atlasSize;
|
|
11898
|
+
const atlas = device.createTexture({
|
|
11899
|
+
label: 'driftLight.atlas',
|
|
11900
|
+
size: [aw, ah, ad],
|
|
11901
|
+
dimension: '3d',
|
|
11902
|
+
format: 'rgba16float',
|
|
11903
|
+
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
11904
|
+
});
|
|
11905
|
+
device.queue.writeTexture({ texture: atlas }, field.atlas, { bytesPerRow: aw * 8, rowsPerImage: ah }, [aw, ah, ad]);
|
|
11906
|
+
this.driftIndex = index;
|
|
11907
|
+
this.driftAtlas = atlas;
|
|
11908
|
+
this.driftIndexView = index.createView({ dimension: '3d' });
|
|
11909
|
+
this.driftAtlasView = atlas.createView({ dimension: '3d' });
|
|
11910
|
+
this.bakeLightField = {
|
|
11911
|
+
index: this.driftIndexView,
|
|
11912
|
+
atlas: this.driftAtlasView,
|
|
11913
|
+
light: this.bakeDriftUniforms.light,
|
|
11914
|
+
origin: this.bakeDriftUniforms.origin,
|
|
11915
|
+
};
|
|
11916
|
+
this.rebuildFlatBindGroup();
|
|
11917
|
+
}
|
|
11460
11918
|
setIesProfiles(profiles) {
|
|
11461
11919
|
if (this.surface.lost)
|
|
11462
11920
|
return;
|
|
@@ -11500,6 +11958,9 @@ export class WebGPURenderer {
|
|
|
11500
11958
|
/** Each level from the one above it, six faces at a time. See `probePass.ts`. */
|
|
11501
11959
|
buildProbeChain(encoder, probe, layer) {
|
|
11502
11960
|
const { device } = this.surface;
|
|
11961
|
+
const targets = this.probeTargets;
|
|
11962
|
+
if (targets === null)
|
|
11963
|
+
return;
|
|
11503
11964
|
/*
|
|
11504
11965
|
* **Reset and flushed here, because a bake is not a frame.**
|
|
11505
11966
|
*
|
|
@@ -11523,12 +11984,8 @@ export class WebGPURenderer {
|
|
|
11523
11984
|
const pipeline = probeMipPipeline(this.pipelines, device, this.probeMipLayout, this.pipelines.format);
|
|
11524
11985
|
const levels = probeLevels(this.probeSize);
|
|
11525
11986
|
for (let level = 1; level < levels; level++) {
|
|
11526
|
-
/*
|
|
11527
|
-
const
|
|
11528
|
-
dimension: 'cube',
|
|
11529
|
-
baseMipLevel: level - 1,
|
|
11530
|
-
mipLevelCount: 1,
|
|
11531
|
-
});
|
|
11987
|
+
/* The whole cube at the level above, so the blur can cross a face edge, in a group kept. */
|
|
11988
|
+
const group = targets.chainGroup(this.probeMipLayout, this.probeMipUniforms.buffer, this.probeSampler, level);
|
|
11532
11989
|
for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
|
|
11533
11990
|
const basis = PROBE_FACE_BASIS[face];
|
|
11534
11991
|
if (basis === undefined)
|
|
@@ -11544,21 +12001,15 @@ export class WebGPURenderer {
|
|
|
11544
12001
|
this.probeMipUniforms.flush();
|
|
11545
12002
|
return;
|
|
11546
12003
|
}
|
|
11547
|
-
this.probeMipUniforms.writeFloats(slot, 0,
|
|
11548
|
-
this.probeMipUniforms.writeFloats(slot, 16,
|
|
11549
|
-
this.probeMipUniforms.writeFloats(slot, 32,
|
|
11550
|
-
this.probeMipUniforms.writeFloats(slot, 48,
|
|
12004
|
+
this.probeMipUniforms.writeFloats(slot, 0, padded(basis.basisX));
|
|
12005
|
+
this.probeMipUniforms.writeFloats(slot, 16, padded(basis.basisY));
|
|
12006
|
+
this.probeMipUniforms.writeFloats(slot, 32, padded(basis.forward));
|
|
12007
|
+
this.probeMipUniforms.writeFloats(slot, 48, ZERO4);
|
|
11551
12008
|
const pass = encoder.beginRenderPass({
|
|
11552
12009
|
label: `probe.mip${level}.face${face}`,
|
|
11553
12010
|
colorAttachments: [
|
|
11554
12011
|
{
|
|
11555
|
-
view:
|
|
11556
|
-
dimension: '2d',
|
|
11557
|
-
baseArrayLayer: face,
|
|
11558
|
-
arrayLayerCount: 1,
|
|
11559
|
-
baseMipLevel: level,
|
|
11560
|
-
mipLevelCount: 1,
|
|
11561
|
-
}),
|
|
12012
|
+
view: targets.chainTarget(level, face),
|
|
11562
12013
|
loadOp: 'clear',
|
|
11563
12014
|
storeOp: 'store',
|
|
11564
12015
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -11566,17 +12017,7 @@ export class WebGPURenderer {
|
|
|
11566
12017
|
],
|
|
11567
12018
|
});
|
|
11568
12019
|
pass.setPipeline(pipeline);
|
|
11569
|
-
pass.setBindGroup(0,
|
|
11570
|
-
layout: this.probeMipLayout,
|
|
11571
|
-
entries: [
|
|
11572
|
-
{
|
|
11573
|
-
binding: 0,
|
|
11574
|
-
resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
|
|
11575
|
-
},
|
|
11576
|
-
{ binding: 1, resource: source },
|
|
11577
|
-
{ binding: 2, resource: this.probeSampler },
|
|
11578
|
-
],
|
|
11579
|
-
}), [slot]);
|
|
12020
|
+
pass.setBindGroup(0, group, [slot]);
|
|
11580
12021
|
pass.draw(3);
|
|
11581
12022
|
pass.end();
|
|
11582
12023
|
}
|
|
@@ -11597,8 +12038,8 @@ export class WebGPURenderer {
|
|
|
11597
12038
|
const prefilterTarget = this.probeArray;
|
|
11598
12039
|
if (prefilterTarget !== null) {
|
|
11599
12040
|
const prefilter = probePrefilterPipeline(this.pipelines, device, this.probePrefilterLayout, this.pipelines.format);
|
|
11600
|
-
/* The whole chain, so a wide sample can select a level from it. */
|
|
11601
|
-
const
|
|
12041
|
+
/* The whole chain, so a wide sample can select a level from it, in a group kept. */
|
|
12042
|
+
const group = targets.prefilterGroupFor(this.probePrefilterLayout, this.probeMipUniforms.buffer, this.probeSampler);
|
|
11602
12043
|
const offsets = PREFILTER_UNIFORM_OFFSETS;
|
|
11603
12044
|
const topLevel = levels - 1;
|
|
11604
12045
|
const samples = this.quality.environmentPrefilterSamples;
|
|
@@ -11606,12 +12047,18 @@ export class WebGPURenderer {
|
|
|
11606
12047
|
const irradianceLevel = irradianceLevelFor(edge);
|
|
11607
12048
|
const ggxTop = ggxMaxLevelFor(edge);
|
|
11608
12049
|
const boxChain = !this.quality.environmentPrefilter;
|
|
12050
|
+
/*
|
|
12051
|
+
* **The diffuse level of a layer the trace has written is the trace's**, so a rasterised bake
|
|
12052
|
+
* stops one level short of it. See `ProbeBaker.traced`: two writers of one level was light
|
|
12053
|
+
* sweeping down a courtyard a probe at a time with the clock paused.
|
|
12054
|
+
*/
|
|
12055
|
+
const lastLevel = this.probeBaker?.traced(layer) === true ? irradianceLevel - 1 : irradianceLevel;
|
|
11609
12056
|
/*
|
|
11610
12057
|
* **One draw a level, where the cube needed six.** An octahedral map has no faces, so the
|
|
11611
12058
|
* six-way loop is gone rather than moved, and a probe's convolution is six times fewer
|
|
11612
12059
|
* passes than the cube it replaces.
|
|
11613
12060
|
*/
|
|
11614
|
-
for (let level = 0; level <=
|
|
12061
|
+
for (let level = 0; level <= lastLevel; level++) {
|
|
11615
12062
|
const irradiance = level === irradianceLevel;
|
|
11616
12063
|
const roughness = irradiance ? 1 : roughnessForLevel(level, ggxTop);
|
|
11617
12064
|
const slot = this.probeMipUniforms.allocate();
|
|
@@ -11628,31 +12075,19 @@ export class WebGPURenderer {
|
|
|
11628
12075
|
/* Written at the generator's own offsets rather than at literals, because a roughness at
|
|
11629
12076
|
the wrong offset is a level convolved for some other roughness — which looks like a
|
|
11630
12077
|
plausible reflection and points at nothing. */
|
|
11631
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterEdge.offset,
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
this.probeMipUniforms.writeFloats(slot, offsets.
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
this.probeMipUniforms.writeFloats(slot, offsets.
|
|
11638
|
-
this.probeMipUniforms.writeFloats(slot, offsets.
|
|
11639
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterRoughness.offset, [roughness]);
|
|
11640
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSamples.offset, [samples]);
|
|
11641
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceTexels.offset, [
|
|
11642
|
-
this.probeSize,
|
|
11643
|
-
]);
|
|
11644
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceMaxLod.offset, [topLevel]);
|
|
12078
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterEdge.offset, one(Math.max(1, edge >> level)));
|
|
12079
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterIrradiance.offset, one(irradiance ? 1 : 0));
|
|
12080
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterBox.offset, one(boxChain ? 1 : 0));
|
|
12081
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterLevel.offset, one(level));
|
|
12082
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterRoughness.offset, one(roughness));
|
|
12083
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSamples.offset, one(samples));
|
|
12084
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceTexels.offset, one(this.probeSize));
|
|
12085
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceMaxLod.offset, one(topLevel));
|
|
11645
12086
|
const pass = encoder.beginRenderPass({
|
|
11646
12087
|
label: `probe.prefilter${level}.layer${layer}`,
|
|
11647
12088
|
colorAttachments: [
|
|
11648
12089
|
{
|
|
11649
|
-
view:
|
|
11650
|
-
dimension: '2d',
|
|
11651
|
-
baseArrayLayer: layer,
|
|
11652
|
-
arrayLayerCount: 1,
|
|
11653
|
-
baseMipLevel: level,
|
|
11654
|
-
mipLevelCount: 1,
|
|
11655
|
-
}),
|
|
12090
|
+
view: targets.arrayView(prefilterTarget, layer, level),
|
|
11656
12091
|
loadOp: 'clear',
|
|
11657
12092
|
storeOp: 'store',
|
|
11658
12093
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -11660,17 +12095,7 @@ export class WebGPURenderer {
|
|
|
11660
12095
|
],
|
|
11661
12096
|
});
|
|
11662
12097
|
pass.setPipeline(prefilter);
|
|
11663
|
-
pass.setBindGroup(0,
|
|
11664
|
-
layout: this.probePrefilterLayout,
|
|
11665
|
-
entries: [
|
|
11666
|
-
{
|
|
11667
|
-
binding: PREFILTER_UNIFORM_BINDING,
|
|
11668
|
-
resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
|
|
11669
|
-
},
|
|
11670
|
-
{ binding: PREFILTER_TEXTURE_BINDING, resource: source },
|
|
11671
|
-
{ binding: PREFILTER_SAMPLER_BINDING, resource: this.probeSampler },
|
|
11672
|
-
],
|
|
11673
|
-
}), [slot]);
|
|
12098
|
+
pass.setBindGroup(0, group, [slot]);
|
|
11674
12099
|
pass.draw(3);
|
|
11675
12100
|
pass.end();
|
|
11676
12101
|
}
|
|
@@ -12134,7 +12559,7 @@ export class WebGPURenderer {
|
|
|
12134
12559
|
f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.frameHeight);
|
|
12135
12560
|
/* In scene units, which is why an HDR target is what makes a threshold above 1 mean
|
|
12136
12561
|
anything: against a clamped buffer nothing is ever brighter than white. */
|
|
12137
|
-
f[slot(0) + prefilter('uThreshold')] = this.quality.bloomThreshold;
|
|
12562
|
+
f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
|
|
12138
12563
|
/* Each downsample's texel is its *source's*, which is the level above it. */
|
|
12139
12564
|
const down = (name) => this.postField(BLOOM_DOWNSAMPLE_FIELDS, name);
|
|
12140
12565
|
for (let index = 1; index < levels.length; index++) {
|
|
@@ -12210,12 +12635,17 @@ export class WebGPURenderer {
|
|
|
12210
12635
|
drainRegistry(this.computes, (definition) => definition.dispose?.(computeDevice));
|
|
12211
12636
|
this.fieldComposer?.dispose();
|
|
12212
12637
|
this.fieldComposer = null;
|
|
12638
|
+
this.driftIndex?.destroy();
|
|
12639
|
+
this.driftAtlas?.destroy();
|
|
12213
12640
|
this.pass = null;
|
|
12214
12641
|
this.encoder = null;
|
|
12215
12642
|
this.skinPalettes.dispose();
|
|
12216
12643
|
this.gpuTimer.dispose();
|
|
12217
12644
|
this.reflection?.dispose();
|
|
12218
12645
|
this.reflection = null;
|
|
12646
|
+
this.probeTargets?.dispose();
|
|
12647
|
+
this.exposurePass?.dispose();
|
|
12648
|
+
this.exposurePass = null;
|
|
12219
12649
|
this.surface.dispose();
|
|
12220
12650
|
}
|
|
12221
12651
|
}
|