@driftengine/core 4.2.1 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +272 -0
- package/README.md +1 -1
- package/dist/cinematic/cameraPath.d.ts +53 -0
- package/dist/cinematic/cameraPath.js +118 -0
- package/dist/environment/daylight.d.ts +77 -0
- package/dist/environment/daylight.js +77 -0
- package/dist/geometry/meshConcat.d.ts +9 -0
- package/dist/geometry/meshConcat.js +101 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/dist/render/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +166 -1
- package/dist/render/backend/webgl2/renderer.js +424 -23
- package/dist/render/backend/webgpu/buffers.js +46 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
- package/dist/render/backend/webgpu/depthPass.js +75 -14
- package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
- package/dist/render/backend/webgpu/exposurePass.js +190 -0
- package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
- package/dist/render/backend/webgpu/fieldCompose.js +13 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
- package/dist/render/backend/webgpu/flatPass.js +12 -5
- package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
- package/dist/render/backend/webgpu/particlePass.js +1 -0
- package/dist/render/backend/webgpu/postPass.d.ts +12 -2
- package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
- package/dist/render/backend/webgpu/probeBake.js +97 -7
- package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
- package/dist/render/backend/webgpu/probeTargets.js +180 -0
- package/dist/render/backend/webgpu/renderer.d.ts +176 -11
- package/dist/render/backend/webgpu/renderer.js +680 -250
- package/dist/render/backend/webgpu/scatterPass.js +7 -7
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
- package/dist/render/backend/webgpu/skyPass.js +5 -1
- package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
- package/dist/render/backend/webgpu/uniformRing.js +53 -3
- package/dist/render/bloomChain.d.ts +6 -0
- package/dist/render/bloomChain.js +8 -0
- package/dist/render/clusteredLights.d.ts +19 -11
- package/dist/render/clusteredLights.js +96 -45
- package/dist/render/cutoutCaster.d.ts +23 -0
- package/dist/render/cutoutCaster.js +20 -0
- package/dist/render/driftLight/bake.d.ts +34 -0
- package/dist/render/driftLight/bake.js +95 -0
- package/dist/render/driftLight/falloff.d.ts +16 -0
- package/dist/render/driftLight/falloff.js +23 -0
- package/dist/render/driftLight/layout.d.ts +59 -0
- package/dist/render/driftLight/layout.js +138 -0
- package/dist/render/driftLight/lightField.d.ts +108 -0
- package/dist/render/driftLight/lightField.js +179 -0
- package/dist/render/driftLight/uniforms.d.ts +28 -0
- package/dist/render/driftLight/uniforms.js +41 -0
- package/dist/render/driftLight/visibility.d.ts +18 -0
- package/dist/render/driftLight/visibility.js +47 -0
- package/dist/render/emptyTexture.d.ts +6 -0
- package/dist/render/emptyTexture.js +19 -0
- package/dist/render/exposurePass.d.ts +48 -0
- package/dist/render/exposurePass.js +172 -0
- package/dist/render/flameLight.d.ts +71 -0
- package/dist/render/flameLight.js +63 -0
- package/dist/render/gi/bounceLights.d.ts +34 -0
- package/dist/render/gi/bounceLights.js +66 -0
- package/dist/render/gi/fieldReach.d.ts +12 -0
- package/dist/render/gi/fieldReach.js +62 -0
- package/dist/render/gi/globalField.d.ts +8 -0
- package/dist/render/gi/globalField.js +31 -0
- package/dist/render/gi/probeTrace.d.ts +3 -1
- package/dist/render/gi/probeTrace.js +3 -1
- package/dist/render/gpuCapability.js +19 -4
- package/dist/render/lightBudget.d.ts +11 -0
- package/dist/render/lightBudget.js +11 -0
- package/dist/render/pointLightSelection.d.ts +16 -0
- package/dist/render/pointLightSelection.js +178 -83
- package/dist/render/probeGrid.d.ts +11 -1
- package/dist/render/probeGrid.js +5 -2
- package/dist/render/probeSweeps.d.ts +45 -0
- package/dist/render/probeSweeps.js +77 -0
- package/dist/render/reflectionProbe.d.ts +43 -1
- package/dist/render/reflectionProbe.js +17 -3
- package/dist/render/renderQuality.d.ts +3 -1
- package/dist/render/rendererHarness.d.ts +9 -0
- package/dist/render/rendererHarness.js +18 -3
- package/dist/render/sceneTarget.d.ts +20 -0
- package/dist/render/sceneTarget.js +62 -1
- package/dist/render/shaders/agx.d.ts +31 -0
- package/dist/render/shaders/agx.js +81 -0
- package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
- package/dist/render/shaders/ambientOcclusion.js +120 -63
- package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
- package/dist/render/shaders/cutoutCoverage.js +40 -0
- package/dist/render/shaders/depth.d.ts +25 -0
- package/dist/render/shaders/depth.js +69 -3
- package/dist/render/shaders/exposure.d.ts +52 -0
- package/dist/render/shaders/exposure.js +124 -0
- package/dist/render/shaders/filmLook.d.ts +47 -0
- package/dist/render/shaders/filmLook.js +98 -0
- package/dist/render/shaders/flat/directionalShadow.js +35 -4
- package/dist/render/shaders/flat/driftLight.d.ts +29 -0
- package/dist/render/shaders/flat/driftLight.js +75 -0
- package/dist/render/shaders/flat/index.js +5 -0
- package/dist/render/shaders/flat/lobes.js +13 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +68 -9
- package/dist/render/shaders/flat/preamble.js +23 -5
- package/dist/render/shaders/flat/probeGrid.js +46 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
- package/dist/render/shaders/generated/depth.wgsl.js +100 -0
- package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
- package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
- package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
- package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
- package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
- package/dist/render/shaders/generated/rush.wgsl.js +40 -10
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
- package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
- package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
- package/dist/render/shaders/globalMedium.js +4 -0
- package/dist/render/shaders/localExposure.d.ts +35 -0
- package/dist/render/shaders/localExposure.js +138 -0
- package/dist/render/shaders/prefilter.d.ts +1 -1
- package/dist/render/shaders/prefilter.js +6 -0
- package/dist/render/shaders/rush.js +33 -5
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +15 -3
- package/dist/render/shaders/temporalResolve.js +39 -1
- package/dist/render/shadowCasters.d.ts +5 -3
- package/dist/render/shadowMap.js +13 -0
- package/dist/render/surfaceTexture.d.ts +5 -0
- package/package.json +6 -6
- package/scripts/gpuCompute.mjs +24 -3
- package/scripts/png.mjs +23 -12
- package/src/cinematic/cameraPath.ts +178 -0
- package/src/environment/daylight.ts +152 -0
- package/src/geometry/meshConcat.ts +100 -0
- package/src/index.ts +32 -0
- package/src/render/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +462 -20
- package/src/render/backend/webgpu/buffers.ts +51 -8
- package/src/render/backend/webgpu/depthPass.ts +78 -10
- package/src/render/backend/webgpu/exposurePass.ts +234 -0
- package/src/render/backend/webgpu/fieldCompose.ts +15 -0
- package/src/render/backend/webgpu/flatPass.ts +10 -2
- package/src/render/backend/webgpu/particlePass.ts +9 -0
- package/src/render/backend/webgpu/probeBake.ts +143 -9
- package/src/render/backend/webgpu/probeTargets.ts +211 -0
- package/src/render/backend/webgpu/renderer.ts +807 -271
- package/src/render/backend/webgpu/scatterPass.ts +7 -7
- package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
- package/src/render/backend/webgpu/skyPass.ts +5 -1
- package/src/render/backend/webgpu/uniformRing.ts +49 -3
- package/src/render/bloomChain.ts +9 -0
- package/src/render/clusteredLights.ts +108 -63
- package/src/render/cutoutCaster.ts +44 -0
- package/src/render/driftLight/bake.ts +125 -0
- package/src/render/driftLight/falloff.ts +26 -0
- package/src/render/driftLight/layout.ts +177 -0
- package/src/render/driftLight/lightField.ts +227 -0
- package/src/render/driftLight/uniforms.ts +61 -0
- package/src/render/driftLight/visibility.ts +58 -0
- package/src/render/emptyTexture.ts +41 -0
- package/src/render/exposurePass.ts +186 -0
- package/src/render/flameLight.ts +135 -0
- package/src/render/gi/bounceLights.ts +68 -0
- package/src/render/gi/fieldReach.ts +74 -0
- package/src/render/gi/globalField.ts +39 -0
- package/src/render/gi/probeTrace.ts +3 -1
- package/src/render/gpuCapability.ts +18 -5
- package/src/render/lightBudget.ts +12 -0
- package/src/render/pointLightSelection.ts +191 -79
- package/src/render/probeGrid.ts +13 -2
- package/src/render/probeSweeps.ts +80 -0
- package/src/render/reflectionProbe.ts +54 -2
- package/src/render/renderQuality.ts +3 -1
- package/src/render/rendererHarness.ts +25 -3
- package/src/render/sceneTarget.ts +75 -1
- package/src/render/shaders/ambientOcclusion.ts +125 -63
- package/src/render/shaders/cutoutCoverage.ts +43 -0
- package/src/render/shaders/depth.ts +80 -3
- package/src/render/shaders/exposure.ts +128 -0
- package/src/render/shaders/filmLook.ts +111 -0
- package/src/render/shaders/flat/directionalShadow.ts +35 -4
- package/src/render/shaders/flat/driftLight.ts +75 -0
- package/src/render/shaders/flat/index.ts +5 -0
- package/src/render/shaders/flat/lobes.ts +13 -0
- package/src/render/shaders/flat/main.ts +68 -9
- package/src/render/shaders/flat/preamble.ts +23 -5
- package/src/render/shaders/flat/probeGrid.ts +46 -3
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +103 -0
- package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
- package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
- package/src/render/shaders/generated/rush.wgsl.ts +40 -10
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
- package/src/render/shaders/globalMedium.ts +4 -0
- package/src/render/shaders/localExposure.ts +154 -0
- package/src/render/shaders/prefilter.ts +6 -0
- package/src/render/shaders/rush.ts +33 -5
- package/src/render/shaders/tangentFrame.ts +15 -3
- package/src/render/shaders/temporalResolve.ts +39 -1
- package/src/render/shadowCasters.ts +5 -3
- package/src/render/shadowMap.ts +14 -0
- package/src/render/surfaceTexture.ts +5 -0
|
@@ -1,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';
|
|
@@ -38,16 +40,20 @@ import { POINT_SHADOW_SAMPLER, PointShadowSystem, } from '../../pointShadowSyste
|
|
|
38
40
|
import { GpuPointShadowArray, GpuPointShadowMap } from './pointShadowPass.js';
|
|
39
41
|
import { createPanelBindGroup, createPanelBindGroupLayout, createPanelCorners, panelPipeline, PANEL_FRAG_FIELDS, PANEL_FRAG_SIZE, PANEL_VERTEX_COUNT, PANEL_VERT_FIELDS, PANEL_VERT_SIZE, } from './panelPass.js';
|
|
40
42
|
import { createGpuTextCube, createTextBindGroup, createTextBindGroupLayout, GpuText, textPipeline, TEXT_FRAG_FIELDS, TEXT_FRAG_SIZE, TEXT_VERT_FIELDS, TEXT_VERT_SIZE, } from './textPass.js';
|
|
41
|
-
import { deviceSnappedOrigin } from '../../textLayout.js';
|
|
43
|
+
import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.js';
|
|
42
44
|
import { GpuSurfaceTexture } from './surfaceTexturePass.js';
|
|
43
45
|
import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
|
|
44
46
|
import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
|
|
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 { clampAutoExposure } from '../../shaders/exposure.js';
|
|
52
|
+
import { clampLocalExposure } from '../../shaders/localExposure.js';
|
|
48
53
|
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
54
|
import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
|
|
50
|
-
import {
|
|
55
|
+
import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
|
|
56
|
+
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
57
|
import { CAUSTICS_CELL_M, CAUSTICS_MAX_DROP_M } from '../../causticsRenderer.js';
|
|
52
58
|
import { buildSheets } from '../../surfaceSheet.js';
|
|
53
59
|
import { seaStateForWind } from '../../seaState.js';
|
|
@@ -62,13 +68,13 @@ import { DEFAULT_GLOBAL_MEDIUM, mediumActive, mediumTargetSize, WEAK_GPU_MEDIUM_
|
|
|
62
68
|
import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental } from './buffers.js';
|
|
63
69
|
import { SkinPaletteRing } from './skinPaletteRing.js';
|
|
64
70
|
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';
|
|
71
|
+
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
72
|
import { PipelineCache } from './pipelineCache.js';
|
|
67
73
|
import { SKY_FIELDS, SKY_UNIFORM_SIZE, createSkyBindGroup, createSkyBindGroupLayout, skyPipeline, } from './skyPass.js';
|
|
68
74
|
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
75
|
import { WATER_FRAG_FIELDS, WATER_FRAG_SIZE, WATER_VERT_FIELDS, WATER_VERT_SIZE, createGpuWater, createWaterBindGroup, createWaterBindGroupLayout, waterPipeline, } from './waterPass.js';
|
|
70
76
|
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,
|
|
77
|
+
import { PARTICLE_VERT_FIELDS, createGpuParticles, createParticleBindGroupLayout, packInstances, particleFragBindings, particlePipeline, } from './particlePass.js';
|
|
72
78
|
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
79
|
import { FLOCK_FRAG_FIELDS, FLOCK_FRAG_SIZE, FLOCK_VERT_FIELDS, FLOCK_VERT_SIZE, createFlockBindGroup, createFlockBindGroupLayout, createGpuFlock, flockPipeline, } from './flockPass.js';
|
|
74
80
|
import { BOLT_FRAG_FIELDS, BOLT_FRAG_SIZE, BOLT_VERT_FIELDS, BOLT_VERT_SIZE, boltPipeline, createBoltBindGroup, createBoltBindGroupLayout, createGpuBolts, } from './boltPass.js';
|
|
@@ -80,8 +86,11 @@ import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
|
|
|
80
86
|
import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.js';
|
|
81
87
|
import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.js';
|
|
82
88
|
import { DistanceFieldScene } from '../../gi/fieldScene.js';
|
|
83
|
-
import { DEFAULT_FIELD_COMPOSE, FieldComposer } from './fieldCompose.js';
|
|
89
|
+
import { DEFAULT_FIELD_COMPOSE, FieldComposer, MAX_FIELD_CASCADES } from './fieldCompose.js';
|
|
90
|
+
import { cascadesForGrid } from '../../gi/fieldReach.js';
|
|
84
91
|
import { ProbeBaker } from './probeBake.js';
|
|
92
|
+
import { createBounceLights, resolveBounceLights } from '../../gi/bounceLights.js';
|
|
93
|
+
import { ProbeTargets } from './probeTargets.js';
|
|
85
94
|
import { distanceFieldBounds } from '../../gi/fieldScene.js';
|
|
86
95
|
import { fitProbeGrid } from '../../gi/probeGridFit.js';
|
|
87
96
|
/**
|
|
@@ -107,6 +116,9 @@ import { ClusterBinner } from './clusterBinner.js';
|
|
|
107
116
|
import { MAX_CLUSTERED_LIGHTS } from '../../clusteredLights.js';
|
|
108
117
|
import { computeAt, createComputeRegistry, } from '../../compute.js';
|
|
109
118
|
import { GpuInstancedBatch } from './instanced.js';
|
|
119
|
+
import { cutoutOf } from '../../cutoutCaster.js';
|
|
120
|
+
import { LightField } from '../../driftLight/lightField.js';
|
|
121
|
+
import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
|
|
110
122
|
/**
|
|
111
123
|
* How many draws one frame may make before the ring is full.
|
|
112
124
|
*
|
|
@@ -157,6 +169,12 @@ const MAX_SCATTER_DEPTH_DRAWS = 256;
|
|
|
157
169
|
* against the world.
|
|
158
170
|
*/
|
|
159
171
|
const MAX_OVERLAYS = 64;
|
|
172
|
+
/**
|
|
173
|
+
* Sky draws a frame can hold. A frame draws one, a mirror adds one, and a probe bake six a probe —
|
|
174
|
+
* given back after each probe's submit, so a whole grid needs six. Sixty-four is room for insets
|
|
175
|
+
* and passes nobody has written yet; the cost is sixteen kilobytes.
|
|
176
|
+
*/
|
|
177
|
+
const MAX_SKY_DRAWS = 64;
|
|
160
178
|
/**
|
|
161
179
|
* Distinct materials a frame may set.
|
|
162
180
|
*
|
|
@@ -678,8 +696,6 @@ const AO_BLUR_SLOT = DYNAMIC_UNIFORM_ALIGNMENT;
|
|
|
678
696
|
const BLOOM_UPSAMPLE_SLOT = BLOOM_LEVELS;
|
|
679
697
|
/** The tint a draw gets when it asks for none, matching the WebGL2 path's reset-to-white. */
|
|
680
698
|
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
699
|
/** A dust field standing still, for a light volume that asked for dust and not for wind. */
|
|
684
700
|
const NO_DRIFT = new Float32Array([0, 0, 0]);
|
|
685
701
|
/**
|
|
@@ -728,6 +744,23 @@ const NO_CLIP_PLANE = new Float32Array(4);
|
|
|
728
744
|
* matrix the scene drew with and hands over the product.
|
|
729
745
|
*/
|
|
730
746
|
const MOTION_CLIP_FLIP = mat4.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
747
|
+
/* Scratch for the probe blur's and convolution's uniform writes, so a bake writes no array literal. */
|
|
748
|
+
const SCRATCH_ONE = new Float32Array(1);
|
|
749
|
+
const SCRATCH_VEC4 = new Float32Array(4);
|
|
750
|
+
const ZERO4 = new Float32Array(4);
|
|
751
|
+
/** `value` in a one-float scratch, overwritten by the next call. */
|
|
752
|
+
function one(value) {
|
|
753
|
+
SCRATCH_ONE[0] = value;
|
|
754
|
+
return SCRATCH_ONE;
|
|
755
|
+
}
|
|
756
|
+
/** A direction padded to a `vec4`, in a scratch overwritten by the next call. */
|
|
757
|
+
function padded(direction) {
|
|
758
|
+
SCRATCH_VEC4[0] = direction[0] ?? 0;
|
|
759
|
+
SCRATCH_VEC4[1] = direction[1] ?? 0;
|
|
760
|
+
SCRATCH_VEC4[2] = direction[2] ?? 0;
|
|
761
|
+
SCRATCH_VEC4[3] = 0;
|
|
762
|
+
return SCRATCH_VEC4;
|
|
763
|
+
}
|
|
731
764
|
export class WebGPURenderer {
|
|
732
765
|
/**
|
|
733
766
|
* Real timestamp queries, where the adapter offers them.
|
|
@@ -1508,12 +1541,20 @@ export class WebGPURenderer {
|
|
|
1508
1541
|
frameEye = new Float32Array(3);
|
|
1509
1542
|
/** The environment the frame was drawn with. See `bindMeshPass`. */
|
|
1510
1543
|
frameEnv = null;
|
|
1544
|
+
/**
|
|
1545
|
+
* The sky this frame drew, or null until it draws one. A traced probe's escaping rays read it,
|
|
1546
|
+
* so a frame that stops drawing a sky must not go on lighting its probes with the last one.
|
|
1547
|
+
*/
|
|
1548
|
+
frameSky = null;
|
|
1511
1549
|
/** That matrix with the clip-space correction folded in, rebuilt once a frame. */
|
|
1512
1550
|
correctedViewProj = new Float32Array(16);
|
|
1513
1551
|
skyLayout;
|
|
1552
|
+
/**
|
|
1553
|
+
* A slot a sky draw, as every per-draw resource here is: see `UniformRing` and the 2026-08-27 rule.
|
|
1554
|
+
* It was one buffer rewritten at each draw, which gave every sky in an encoder the last camera.
|
|
1555
|
+
*/
|
|
1514
1556
|
skyUniforms;
|
|
1515
|
-
|
|
1516
|
-
skyFloats = new Float32Array(this.skyStaging);
|
|
1557
|
+
skyFloats = new Float32Array(SKY_UNIFORM_SIZE / 4);
|
|
1517
1558
|
skyBindGroup;
|
|
1518
1559
|
/** The camera's inverse with the clip correction undone, rebuilt once a frame. */
|
|
1519
1560
|
skyInvViewProj = new Float32Array(16);
|
|
@@ -1774,35 +1815,75 @@ export class WebGPURenderer {
|
|
|
1774
1815
|
* prefix that has already been uploaded costs one write of unchanged bytes and cannot be
|
|
1775
1816
|
* wrong: a ring only ever appends within a frame.
|
|
1776
1817
|
*/
|
|
1818
|
+
/**
|
|
1819
|
+
* Every per-draw ring the frame writes and flushes together, in one list.
|
|
1820
|
+
*
|
|
1821
|
+
* **One list, read by the flush and by the probe bake's rewind**, so a ring added to one cannot be
|
|
1822
|
+
* forgotten by the other: a ring flushed but never rewound would let a grid bake spend it, and one
|
|
1823
|
+
* rewound but never flushed would upload nothing. Built on first use, after the constructor has
|
|
1824
|
+
* set every field; `growTo` replaces a ring's buffer, never the ring, so the list stays true.
|
|
1825
|
+
*/
|
|
1826
|
+
ringList = null;
|
|
1827
|
+
rings() {
|
|
1828
|
+
this.ringList ??= [
|
|
1829
|
+
this.perDraw,
|
|
1830
|
+
this.lightVolumeVertices,
|
|
1831
|
+
this.lightVolumeFragments,
|
|
1832
|
+
this.plumeVerts,
|
|
1833
|
+
this.plumeFrags,
|
|
1834
|
+
this.waterVerts,
|
|
1835
|
+
this.waterFrags,
|
|
1836
|
+
this.boltVerts,
|
|
1837
|
+
this.boltFrags,
|
|
1838
|
+
this.causticsVerts,
|
|
1839
|
+
this.causticsFrags,
|
|
1840
|
+
this.flockVerts,
|
|
1841
|
+
this.flockFrags,
|
|
1842
|
+
this.windStreakVerts,
|
|
1843
|
+
this.windStreakFrags,
|
|
1844
|
+
this.scatterVerts,
|
|
1845
|
+
this.scatterFrags,
|
|
1846
|
+
this.perFrame,
|
|
1847
|
+
this.filmVerts,
|
|
1848
|
+
this.filmFrags,
|
|
1849
|
+
this.insetUniforms,
|
|
1850
|
+
this.panelVerts,
|
|
1851
|
+
this.panelFrags,
|
|
1852
|
+
this.textVerts,
|
|
1853
|
+
this.textFrags,
|
|
1854
|
+
this.sdfTextVerts,
|
|
1855
|
+
this.sdfTextFrags,
|
|
1856
|
+
this.lineVerts,
|
|
1857
|
+
this.lineFrags,
|
|
1858
|
+
this.skyUniforms,
|
|
1859
|
+
];
|
|
1860
|
+
return this.ringList;
|
|
1861
|
+
}
|
|
1777
1862
|
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();
|
|
1863
|
+
for (const ring of this.rings())
|
|
1864
|
+
ring.flush();
|
|
1865
|
+
}
|
|
1866
|
+
/** Where every ring stands before work that submits an encoder of its own. */
|
|
1867
|
+
ringMarks = [];
|
|
1868
|
+
markRings() {
|
|
1869
|
+
const rings = this.rings();
|
|
1870
|
+
this.ringMarks.length = rings.length;
|
|
1871
|
+
for (let i = 0; i < rings.length; i++)
|
|
1872
|
+
this.ringMarks[i] = rings[i].mark();
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Give back every slot taken since `markRings`, once that work has been submitted.
|
|
1876
|
+
*
|
|
1877
|
+
* See `UniformRing.rewind` for why this is safe only after the submit. The palette ring goes back
|
|
1878
|
+
* with them, and the cached palette slot is dropped if it was one of those given back.
|
|
1879
|
+
*/
|
|
1880
|
+
rewindRings(skinMark) {
|
|
1881
|
+
const rings = this.rings();
|
|
1882
|
+
for (let i = 0; i < rings.length; i++)
|
|
1883
|
+
rings[i].rewind(this.ringMarks[i] ?? 0);
|
|
1884
|
+
this.skinPalettes.rewind(skinMark);
|
|
1885
|
+
if (this.skinPaletteSlot >= skinMark)
|
|
1886
|
+
this.skinPaletteSlot = -1;
|
|
1806
1887
|
}
|
|
1807
1888
|
/* -- The film ------------------------------------------------------------------------- */
|
|
1808
1889
|
filmLayout;
|
|
@@ -1970,6 +2051,8 @@ export class WebGPURenderer {
|
|
|
1970
2051
|
blankAlbedoBindGroup;
|
|
1971
2052
|
/** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
|
|
1972
2053
|
uvScale = new Float32Array([1, 1]);
|
|
2054
|
+
/** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
|
|
2055
|
+
materialDoubleSided = false;
|
|
1973
2056
|
createSurfaceTexture(source, options = {}) {
|
|
1974
2057
|
return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
|
|
1975
2058
|
}
|
|
@@ -2032,6 +2115,8 @@ export class WebGPURenderer {
|
|
|
2032
2115
|
* a group holding the old view hands a destroyed texture to the next draw that asks.
|
|
2033
2116
|
*/
|
|
2034
2117
|
forgetBindingsOf(texture) {
|
|
2118
|
+
this.cutoutGroups.delete(texture);
|
|
2119
|
+
this.cutoutPeelGroups.delete(texture);
|
|
2035
2120
|
/* Same reasoning, for whichever SDF labels were bound against this atlas. */
|
|
2036
2121
|
this.sdfTextBindGroups.delete(texture);
|
|
2037
2122
|
this.flatBindGroups.clear();
|
|
@@ -2088,12 +2173,14 @@ export class WebGPURenderer {
|
|
|
2088
2173
|
const albedo = material?.albedo ?? null;
|
|
2089
2174
|
this.uvScale[0] = material?.uScale ?? 1;
|
|
2090
2175
|
this.uvScale[1] = material?.vScale ?? 1;
|
|
2176
|
+
this.materialDoubleSided = material?.doubleSided === true;
|
|
2091
2177
|
const normal = (material?.normal ?? null);
|
|
2092
2178
|
const orm = (material?.orm ?? null);
|
|
2093
2179
|
const emissiveMap = (material?.emissive ?? null);
|
|
2094
2180
|
this.material((f, i) => {
|
|
2095
2181
|
i[this.materialField('uAlbedoEnabled')] = albedo === null ? 0 : 1;
|
|
2096
2182
|
f[this.materialField('uAlbedoCutout')] = material?.cutout ?? 0;
|
|
2183
|
+
i[this.materialField('uDoubleSided')] = material?.doubleSided === true ? 1 : 0;
|
|
2097
2184
|
/* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
|
|
2098
2185
|
f[this.materialField('uNormalStrength')] =
|
|
2099
2186
|
normal === null ? 0 : (material?.normalStrength ?? 1);
|
|
@@ -2342,6 +2429,10 @@ export class WebGPURenderer {
|
|
|
2342
2429
|
setPlate(text, widthCells, heightCells, bottomCell) {
|
|
2343
2430
|
text.setPlate(widthCells, heightCells, bottomCell);
|
|
2344
2431
|
}
|
|
2432
|
+
/** See `Renderer.snapTextCellSize`; only the drawing buffer differs. */
|
|
2433
|
+
snapTextCellSize(cellSize, viewportWidth) {
|
|
2434
|
+
return deviceSnappedCellSize(cellSize, viewportWidth, this.surface.canvas.width);
|
|
2435
|
+
}
|
|
2345
2436
|
textWidth(text, cellSize) {
|
|
2346
2437
|
return text.layout.widthPx(cellSize);
|
|
2347
2438
|
}
|
|
@@ -2858,6 +2949,16 @@ export class WebGPURenderer {
|
|
|
2858
2949
|
drawParticles(batch, data, camera, env, timeSeconds) {
|
|
2859
2950
|
if (!this.canDraw() || data.count === 0)
|
|
2860
2951
|
return;
|
|
2952
|
+
if (batch.drawnInEncoder === this.encoderEpoch) {
|
|
2953
|
+
if (!this.warnedParticlesTwice) {
|
|
2954
|
+
this.warnedParticlesTwice = true;
|
|
2955
|
+
console.warn('WebGPU: a particle batch was drawn twice into one encoder — a mirror and the view, or a ' +
|
|
2956
|
+
"probe's faces — and the second draw is skipped, because both would show the second " +
|
|
2957
|
+
"draw's particles. Give each view its own batch.");
|
|
2958
|
+
}
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
batch.drawnInEncoder = this.encoderEpoch;
|
|
2861
2962
|
const live = packInstances(batch, data);
|
|
2862
2963
|
if (live === 0)
|
|
2863
2964
|
return;
|
|
@@ -3484,7 +3585,6 @@ export class WebGPURenderer {
|
|
|
3484
3585
|
v[at('uCellSize')] = streaks.cellSize;
|
|
3485
3586
|
v[at('uTime')] = timeSeconds;
|
|
3486
3587
|
this.windStreakFragFloats.set(tint, (WIND_STREAK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
|
|
3487
|
-
const { queue } = this.surface.device;
|
|
3488
3588
|
this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
|
|
3489
3589
|
this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
|
|
3490
3590
|
const windPipe = windStreakPipeline(this.pipelines, this.surface.device, this.windStreakLayout);
|
|
@@ -3570,7 +3670,6 @@ export class WebGPURenderer {
|
|
|
3570
3670
|
v[at('uWind')] = windX;
|
|
3571
3671
|
v[at('uWind') + 1] = windZ;
|
|
3572
3672
|
this.flockFragFloats.set(tint, (FLOCK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
|
|
3573
|
-
const { queue } = this.surface.device;
|
|
3574
3673
|
this.flockVerts.writeBlock(vertexSlot, this.flockVertBlock);
|
|
3575
3674
|
this.flockFrags.writeBlock(fragmentSlot, this.flockFragBlock);
|
|
3576
3675
|
const flockPipe = flockPipeline(this.pipelines, this.surface.device, this.flockLayout);
|
|
@@ -3864,7 +3963,8 @@ export class WebGPURenderer {
|
|
|
3864
3963
|
scatterLayout;
|
|
3865
3964
|
/** One slot per batch: the gust and the trample field belong to the batch, not the frame. */
|
|
3866
3965
|
scatterVerts;
|
|
3867
|
-
|
|
3966
|
+
/** A slot a draw, beside the vertex block's: see `createScatterBindGroupLayout` for why. */
|
|
3967
|
+
scatterFrags;
|
|
3868
3968
|
scatterFragStaging = new ArrayBuffer(SCATTER_FRAG_SIZE);
|
|
3869
3969
|
scatterFragFloats = new Float32Array(this.scatterFragStaging);
|
|
3870
3970
|
scatterFragInts = new Int32Array(this.scatterFragStaging);
|
|
@@ -4049,17 +4149,28 @@ export class WebGPURenderer {
|
|
|
4049
4149
|
dynamicView;
|
|
4050
4150
|
/** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
|
|
4051
4151
|
shadowPeelBindGroup;
|
|
4152
|
+
/**
|
|
4153
|
+
* The cutout casters' layout, and a bind group per cutout map for each layer, built the first
|
|
4154
|
+
* time a map casts and kept: a `createBindGroup` per draw is the allocation the house rules forbid.
|
|
4155
|
+
* Cleared in `forgetBindingsOf`, because a group outliving its texture is a destroyed-texture error
|
|
4156
|
+
* at submit, and a texture's view is new when an update changes its size.
|
|
4157
|
+
*/
|
|
4158
|
+
shadowCutoutLayout;
|
|
4159
|
+
cutoutGroups = new Map();
|
|
4160
|
+
cutoutPeelGroups = new Map();
|
|
4161
|
+
shadowPeelPassUniforms = null;
|
|
4162
|
+
shadowPeelSampler = null;
|
|
4052
4163
|
scatterDepthPeelBindGroup;
|
|
4053
4164
|
/** Which layer the open pass is writing, so the sink picks the bindings that match it. */
|
|
4054
4165
|
shadowLayerIsPeel = false;
|
|
4055
4166
|
/**
|
|
4056
4167
|
* Whether a peel pass has filled the map, which is what the frame may sample it on.
|
|
4057
4168
|
*
|
|
4058
|
-
* **Not whether the map exists.** `renderer.ts`
|
|
4059
|
-
* born
|
|
4060
|
-
*
|
|
4061
|
-
* static layer opens and set when a peel pass ends, which
|
|
4062
|
-
*
|
|
4169
|
+
* **Not whether the map exists.** `renderer.ts` asked that question, on the belief that a GL
|
|
4170
|
+
* texture is born reading as the far plane, and it is born zeroed: a scene that built a peel
|
|
4171
|
+
* map and never opened the layer had no sun at all under ANGLE's GL backend. It holds this
|
|
4172
|
+
* rule now as well. Cleared when the static layer opens and set when a peel pass ends, which
|
|
4173
|
+
* is per frame because every scene that peels does so every frame.
|
|
4063
4174
|
*/
|
|
4064
4175
|
peelFilled = false;
|
|
4065
4176
|
/** One slot per caster, the same trick `perDraw` uses and for the same reason. */
|
|
@@ -4100,6 +4211,28 @@ export class WebGPURenderer {
|
|
|
4100
4211
|
* otherwise. A lookup rather than a rebind, because a `createBindGroup` per draw is the
|
|
4101
4212
|
* per-frame allocation the house rules forbid; both groups are built once at init.
|
|
4102
4213
|
*/
|
|
4214
|
+
/** The cutout bind group for `texture` in the open layer, built the first time it casts there. */
|
|
4215
|
+
cutoutGroup(texture) {
|
|
4216
|
+
const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
|
|
4217
|
+
const groups = peel ? this.cutoutPeelGroups : this.cutoutGroups;
|
|
4218
|
+
const held = groups.get(texture);
|
|
4219
|
+
if (held !== undefined)
|
|
4220
|
+
return held;
|
|
4221
|
+
const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
|
|
4222
|
+
const previousView = peel ? this.shadowView : this.shadowBlankView;
|
|
4223
|
+
const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
|
|
4224
|
+
if (passUniforms === null || previousView === null || previousSampler === null)
|
|
4225
|
+
return null;
|
|
4226
|
+
const group = createDepthBindGroup(this.surface.device, this.shadowCutoutLayout, this.shadowDraws.buffer, passUniforms, previousView, previousSampler, null, { view: texture.view, sampler: texture.sampler });
|
|
4227
|
+
groups.set(texture, group);
|
|
4228
|
+
return group;
|
|
4229
|
+
}
|
|
4230
|
+
/** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
|
|
4231
|
+
writeCutout(slot, cutout, fields) {
|
|
4232
|
+
this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
|
|
4233
|
+
this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
|
|
4234
|
+
this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
|
|
4235
|
+
}
|
|
4103
4236
|
shadowGroup() {
|
|
4104
4237
|
return this.shadowLayerIsPeel && this.shadowPeelBindGroup !== null
|
|
4105
4238
|
? this.shadowPeelBindGroup
|
|
@@ -4135,21 +4268,29 @@ export class WebGPURenderer {
|
|
|
4135
4268
|
* every frame would be an allocation in a per-frame path.
|
|
4136
4269
|
*/
|
|
4137
4270
|
casterSink = {
|
|
4138
|
-
mesh: (mesh, model) => {
|
|
4271
|
+
mesh: (mesh, model, material) => {
|
|
4139
4272
|
const pass = this.shadowPass;
|
|
4140
4273
|
if (pass === null)
|
|
4141
4274
|
return;
|
|
4275
|
+
/* A cutout caster's group is looked up before a slot is spent, so a miss costs nothing. */
|
|
4276
|
+
const cutout = cutoutOf(material);
|
|
4277
|
+
const cutoutGroup = cutout === null ? null : this.cutoutGroup(cutout.albedo);
|
|
4142
4278
|
const slot = this.shadowDraws.allocate();
|
|
4143
4279
|
if (slot === null)
|
|
4144
4280
|
return;
|
|
4145
4281
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
|
|
4146
4282
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model);
|
|
4147
4283
|
this.writeShadowWind(slot);
|
|
4284
|
+
if (cutout !== null && cutoutGroup !== null)
|
|
4285
|
+
this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
|
|
4148
4286
|
const geometry = mesh;
|
|
4149
4287
|
if (geometry.vertexBuffers === undefined)
|
|
4150
4288
|
return;
|
|
4151
|
-
|
|
4152
|
-
|
|
4289
|
+
const cuts = cutoutGroup !== null;
|
|
4290
|
+
/* A two-sided caster casts from whichever face the light sees: nothing culled. */
|
|
4291
|
+
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
4292
|
+
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));
|
|
4293
|
+
pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
|
|
4153
4294
|
for (let index = 0; index < geometry.vertexBuffers.length; index++) {
|
|
4154
4295
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
|
|
4155
4296
|
}
|
|
@@ -4160,7 +4301,7 @@ export class WebGPURenderer {
|
|
|
4160
4301
|
* A batch of rigid meshes into the depth map, placed by the same matrices the visible draw
|
|
4161
4302
|
* uses — one draw, however many instances, exactly as the colour pass does it.
|
|
4162
4303
|
*/
|
|
4163
|
-
instanced: (batch, data) => {
|
|
4304
|
+
instanced: (batch, data, material) => {
|
|
4164
4305
|
const pass = this.shadowPass;
|
|
4165
4306
|
if (pass === null)
|
|
4166
4307
|
return;
|
|
@@ -4171,14 +4312,21 @@ export class WebGPURenderer {
|
|
|
4171
4312
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
4172
4313
|
if (count === 0)
|
|
4173
4314
|
return;
|
|
4315
|
+
const cutout = cutoutOf(material);
|
|
4316
|
+
const cutoutGroup = cutout === null ? null : this.cutoutGroup(cutout.albedo);
|
|
4174
4317
|
const slot = this.shadowDraws.allocate();
|
|
4175
4318
|
if (slot === null)
|
|
4176
4319
|
return;
|
|
4177
4320
|
/* Only the light matrix: the instanced depth variant has no uModel, its placement being
|
|
4178
4321
|
the four attribute columns the batch's buffer supplies. */
|
|
4179
4322
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
|
|
4180
|
-
|
|
4181
|
-
|
|
4323
|
+
if (cutout !== null && cutoutGroup !== null) {
|
|
4324
|
+
this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
|
|
4325
|
+
}
|
|
4326
|
+
const cuts = cutoutGroup !== null;
|
|
4327
|
+
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
4328
|
+
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));
|
|
4329
|
+
pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
|
|
4182
4330
|
let index = 0;
|
|
4183
4331
|
for (; index < geometry.vertexBuffers.length; index++) {
|
|
4184
4332
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
|
|
@@ -4607,28 +4755,6 @@ export class WebGPURenderer {
|
|
|
4607
4755
|
*/
|
|
4608
4756
|
const blankView = blank.createView();
|
|
4609
4757
|
const blankSampler = device.createSampler({ label: 'flat.sampler' });
|
|
4610
|
-
/*
|
|
4611
|
-
* A one-texel cube for every point-shadow sampler the variant declares.
|
|
4612
|
-
*
|
|
4613
|
-
* Twelve of them exist in the `pointShadows` permutation and none has a map behind it
|
|
4614
|
-
* until row 3, but WebGPU requires every declared binding to be filled — and a cube view
|
|
4615
|
-
* needs six array layers, so the flat 1×1 stand-in above cannot serve.
|
|
4616
|
-
*
|
|
4617
|
-
* **White is the second guard, and it is a deliberate value rather than a spare one.** The
|
|
4618
|
-
* first is `uPointShadowIndex`, written to −1 for every light in `bindMeshPass`. If that
|
|
4619
|
-
* ever failed, a full-depth texel reads as an occluder at the far plane — `pointShadow`
|
|
4620
|
-
* converts the stored value back to a distance and 1.0 comes out as `far` — so the worst a
|
|
4621
|
-
* mis-selected light can do is find nothing in the way. Zeroes would have meant an
|
|
4622
|
-
* occluder pressed against the lamp, and every surface in the scene in shadow.
|
|
4623
|
-
*/
|
|
4624
|
-
const blankCube = device.createTexture({
|
|
4625
|
-
label: 'flat.emptyPointShadow',
|
|
4626
|
-
size: [1, 1, 6],
|
|
4627
|
-
format: 'rgba8unorm',
|
|
4628
|
-
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
4629
|
-
});
|
|
4630
|
-
device.queue.writeTexture({ texture: blankCube }, new Uint8Array(4 * 6).fill(255), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 6]);
|
|
4631
|
-
const blankCubeView = blankCube.createView({ dimension: 'cube' });
|
|
4632
4758
|
/*
|
|
4633
4759
|
* A one-texel integer stand-in for the froxel table.
|
|
4634
4760
|
*
|
|
@@ -4667,6 +4793,16 @@ export class WebGPURenderer {
|
|
|
4667
4793
|
});
|
|
4668
4794
|
device.queue.writeTexture({ texture: blankArray }, new Uint8Array(4).fill(255), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 1]);
|
|
4669
4795
|
const blankArrayView = blankArray.createView({ dimension: '2d-array' });
|
|
4796
|
+
/*
|
|
4797
|
+
* DriftLight's two volumes before a field is whole: an index of one empty cell and an atlas
|
|
4798
|
+
* of one zero texel. Never read while `uDriftLight.x` is zero, and bound anyway, because a
|
|
4799
|
+
* group missing an entry its layout declares is a validation failure at `submit`.
|
|
4800
|
+
*/
|
|
4801
|
+
const blankVolume = (label, format) => device
|
|
4802
|
+
.createTexture({ label, size: [1, 1, 1], dimension: '3d', format, usage: 0x2 | 0x4 })
|
|
4803
|
+
.createView({ dimension: '3d' });
|
|
4804
|
+
const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
|
|
4805
|
+
const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
|
|
4670
4806
|
this.shadowMap = device.createTexture({
|
|
4671
4807
|
label: 'shadow.static',
|
|
4672
4808
|
size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
|
|
@@ -4704,14 +4840,15 @@ export class WebGPURenderer {
|
|
|
4704
4840
|
: null;
|
|
4705
4841
|
this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
|
|
4706
4842
|
/*
|
|
4707
|
-
* **Cleared once, here, because a texture no pass has written
|
|
4843
|
+
* **Cleared once, here, because a texture no pass has written reads as zero**, which the
|
|
4844
|
+
* specification requires, and a zero in a map compared with `LEQUAL` is an occluder at the
|
|
4845
|
+
* light. A scene is entitled to switch directional shadows *on* and never open a pass —
|
|
4846
|
+
* `night-street` does exactly that, with `shadowStrength` at 0.85 and no `beginShadowPass`
|
|
4847
|
+
* anywhere in it — and sampling that zero shadows everything, which is a picture rather than
|
|
4848
|
+
* an error and reads as a lighting bug.
|
|
4708
4849
|
*
|
|
4709
|
-
*
|
|
4710
|
-
*
|
|
4711
|
-
* no such promise, and a scene is entitled to switch directional shadows *on* and never
|
|
4712
|
-
* open a pass — `night-street` does exactly that, with `shadowStrength` at 0.85 and no
|
|
4713
|
-
* `beginShadowPass` anywhere in it. Sampling garbage then shadows whatever the garbage
|
|
4714
|
-
* happens to say, which is a picture rather than an error and reads as a lighting bug.
|
|
4850
|
+
* `shadowMap.ts` does the same for the other backend, for the same reason found later: WebGL
|
|
4851
|
+
* zeroes a new texture too, and only ANGLE on Vulkan happened to hand one back reading 1.
|
|
4715
4852
|
*
|
|
4716
4853
|
* Found by dumping the flat block off both live renderers and comparing it field by field;
|
|
4717
4854
|
* `uPeeledShadowEnabled` came back 1 against 0 and pointed straight at the maps behind it.
|
|
@@ -4822,7 +4959,7 @@ export class WebGPURenderer {
|
|
|
4822
4959
|
* is writing, so the binding has to move whatever the shader intends to do with it.
|
|
4823
4960
|
*/
|
|
4824
4961
|
if (name === 'uEnvironment') {
|
|
4825
|
-
return this.probeView === null || !this.
|
|
4962
|
+
return this.probeView === null || !this.probeReadable()
|
|
4826
4963
|
? { view: blankArrayView, sampler: blankSampler }
|
|
4827
4964
|
: { view: this.probeView, sampler: this.probeSampler };
|
|
4828
4965
|
}
|
|
@@ -4856,6 +4993,17 @@ export class WebGPURenderer {
|
|
|
4856
4993
|
if (name === 'uCookieAtlas') {
|
|
4857
4994
|
return { view: this.cookieView ?? blankView, sampler: iesSampler };
|
|
4858
4995
|
}
|
|
4996
|
+
/*
|
|
4997
|
+
* DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
|
|
4998
|
+
* sampler the way the froxel table does; and its atlas, which is filtered inside a brick and
|
|
4999
|
+
* so takes the linear, clamped one the photometric atlas uses.
|
|
5000
|
+
*/
|
|
5001
|
+
if (name === 'uDriftLightIndex') {
|
|
5002
|
+
return { view: this.driftIndexView ?? blankDriftIndexView, sampler: shadowSampler };
|
|
5003
|
+
}
|
|
5004
|
+
if (name === 'uDriftLightAtlas') {
|
|
5005
|
+
return { view: this.driftAtlasView ?? blankDriftAtlasView, sampler: iesSampler };
|
|
5006
|
+
}
|
|
4859
5007
|
/*
|
|
4860
5008
|
* The colour the frame had already drawn, or the blank texel a frame that refracts nothing
|
|
4861
5009
|
* falls back to.
|
|
@@ -4910,12 +5058,8 @@ export class WebGPURenderer {
|
|
|
4910
5058
|
/* The same group: no texture is set at construction, so this is the stand-in's. */
|
|
4911
5059
|
this.blankAlbedoBindGroup = this.bindGroup;
|
|
4912
5060
|
this.skyLayout = createSkyBindGroupLayout(device);
|
|
4913
|
-
this.skyUniforms = device.
|
|
4914
|
-
|
|
4915
|
-
size: SKY_UNIFORM_SIZE,
|
|
4916
|
-
usage: USAGE_UNIFORM_DST,
|
|
4917
|
-
});
|
|
4918
|
-
this.skyBindGroup = createSkyBindGroup(device, this.skyLayout, this.skyUniforms);
|
|
5061
|
+
this.skyUniforms = new UniformRing(device, SKY_UNIFORM_SIZE, MAX_SKY_DRAWS, USAGE_UNIFORM_DST, 'sky.ring');
|
|
5062
|
+
this.skyBindGroup = createSkyBindGroup(device, this.skyLayout, this.skyUniforms.buffer);
|
|
4919
5063
|
this.plumeLayout = createPlumeBindGroupLayout(device);
|
|
4920
5064
|
this.plumeVerts = new UniformRing(device, PLUME_VERT_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
|
|
4921
5065
|
this.plumeFrags = new UniformRing(device, PLUME_FRAG_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
|
|
@@ -4954,6 +5098,8 @@ export class WebGPURenderer {
|
|
|
4954
5098
|
usage: 0x10 | 0x4 | 0x2, // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_DST
|
|
4955
5099
|
})
|
|
4956
5100
|
: null;
|
|
5101
|
+
this.probeTargets =
|
|
5102
|
+
this.probe === null ? null : new ProbeTargets(device, this.probe, this.probeSize);
|
|
4957
5103
|
/*
|
|
4958
5104
|
* The prefiltered twin, allocated on exactly the condition the capture is and with exactly
|
|
4959
5105
|
* its format and level count. Same format because a pipeline's colour target must equal its
|
|
@@ -4997,6 +5143,10 @@ export class WebGPURenderer {
|
|
|
4997
5143
|
is not a screen-sized target. Filtering is what makes a 32-lattice table smooth rather
|
|
4998
5144
|
than banded, so it is declared filterable like the colour inputs and unlike the depth. */
|
|
4999
5145
|
{ binding: RUSH_TEXTURES.uGradeLut, dimension: '3d' },
|
|
5146
|
+
/* Eye adaptation's held brightness: one half-float texel, or the scene where none is held. */
|
|
5147
|
+
{ binding: RUSH_TEXTURES.uExposureHeld },
|
|
5148
|
+
/* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
|
|
5149
|
+
{ binding: RUSH_TEXTURES.uExposureLocal },
|
|
5000
5150
|
]);
|
|
5001
5151
|
this.rushUniforms = device.createBuffer({
|
|
5002
5152
|
label: 'post.rushUniforms',
|
|
@@ -5163,15 +5313,14 @@ export class WebGPURenderer {
|
|
|
5163
5313
|
this.filmBindGroupBlank = this.filmBindGroup;
|
|
5164
5314
|
this.scatterLayout = createScatterBindGroupLayout(device);
|
|
5165
5315
|
this.scatterVerts = new UniformRing(device, SCATTER_VERT_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
|
|
5166
|
-
this.
|
|
5167
|
-
|
|
5168
|
-
size: SCATTER_FRAG_SIZE,
|
|
5169
|
-
usage: USAGE_UNIFORM_DST,
|
|
5170
|
-
});
|
|
5171
|
-
this.scatterBindGroup = createScatterBindGroup(device, this.scatterLayout, this.scatterVerts.buffer, this.scatterFragUniforms);
|
|
5316
|
+
this.scatterFrags = new UniformRing(device, SCATTER_FRAG_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST, 'scatter.fragRing');
|
|
5317
|
+
this.scatterBindGroup = createScatterBindGroup(device, this.scatterLayout, this.scatterVerts.buffer, this.scatterFrags.buffer);
|
|
5172
5318
|
this.shadowLayout = createDepthBindGroupLayout(device);
|
|
5173
5319
|
this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
|
|
5174
|
-
this.
|
|
5320
|
+
this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
|
|
5321
|
+
this.shadowDraws = new UniformRing(device,
|
|
5322
|
+
/* The cutout block is the plain one and two fields more, and a slot must hold either. */
|
|
5323
|
+
Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE), MAX_DRAWS_PER_FRAME, USAGE_UNIFORM_DST, 'shadow.drawRing', this.shadowDrawBudget);
|
|
5175
5324
|
const depthPassUniforms = device.createBuffer({
|
|
5176
5325
|
label: 'depth.passUniforms',
|
|
5177
5326
|
size: DEPTH_FRAG_SIZE,
|
|
@@ -5209,6 +5358,8 @@ export class WebGPURenderer {
|
|
|
5209
5358
|
if (peelPassUniforms !== null) {
|
|
5210
5359
|
device.queue.writeBuffer(peelPassUniforms, 0, new Int32Array([1]));
|
|
5211
5360
|
}
|
|
5361
|
+
this.shadowPeelPassUniforms = peelPassUniforms;
|
|
5362
|
+
this.shadowPeelSampler = shadowSampler;
|
|
5212
5363
|
this.shadowPeelBindGroup =
|
|
5213
5364
|
peelPassUniforms === null
|
|
5214
5365
|
? null
|
|
@@ -5217,13 +5368,6 @@ export class WebGPURenderer {
|
|
|
5217
5368
|
peelPassUniforms === null
|
|
5218
5369
|
? null
|
|
5219
5370
|
: createScatterDepthBindGroup(device, this.scatterDepthLayout, this.scatterDepthDraws.buffer, { binding: DEPTH_FRAG_BINDING, buffer: peelPassUniforms, size: DEPTH_FRAG_SIZE }, DEPTH_PREVIOUS_BINDING, this.shadowView, shadowSampler);
|
|
5220
|
-
/*
|
|
5221
|
-
* The light volume, last because it wants the shadow map and the stand-in that everything
|
|
5222
|
-
* above already built. The peel and dynamic maps get the white one-pixel texture, which is
|
|
5223
|
-
* depth 1 and therefore nothing in the way — see `createLightVolumeBindGroup` for why a
|
|
5224
|
-
* stand-in of zero would put every beam in shadow instead.
|
|
5225
|
-
*/
|
|
5226
|
-
const uniformBuffer = (label, size) => device.createBuffer({ label, size, usage: USAGE_UNIFORM_DST });
|
|
5227
5371
|
/*
|
|
5228
5372
|
* The overlays. Rings rather than single blocks because a frame draws several of each —
|
|
5229
5373
|
* the loader alone is two panels and two strings — and `queue.writeBuffer` does not
|
|
@@ -5385,6 +5529,7 @@ export class WebGPURenderer {
|
|
|
5385
5529
|
this.reflectionQueue.reset();
|
|
5386
5530
|
/* Redeclared every frame, so a field a consumer stops declaring stops lighting. */
|
|
5387
5531
|
this.distanceFields.reset();
|
|
5532
|
+
this.frameSky = null;
|
|
5388
5533
|
this.oitActive =
|
|
5389
5534
|
this.quality.orderIndependent && this.quality.screenEffects && this.samples === 1;
|
|
5390
5535
|
/* Read off the line above rather than restating its last term: with the other two true, the
|
|
@@ -5406,6 +5551,7 @@ export class WebGPURenderer {
|
|
|
5406
5551
|
/* Anything drawn after the last `endFrame` goes now, before its encoder is replaced. */
|
|
5407
5552
|
this.flushOverlay();
|
|
5408
5553
|
this.framePresented = false;
|
|
5554
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
5409
5555
|
/* Before the budget is cleared, because what to grow to is what the last frame asked for. */
|
|
5410
5556
|
this.growRings();
|
|
5411
5557
|
this.budget.reset();
|
|
@@ -5445,6 +5591,7 @@ export class WebGPURenderer {
|
|
|
5445
5591
|
this.windStreakVerts.reset();
|
|
5446
5592
|
this.windStreakFrags.reset();
|
|
5447
5593
|
this.scatterVerts.reset();
|
|
5594
|
+
this.scatterFrags.reset();
|
|
5448
5595
|
this.panelVerts.reset();
|
|
5449
5596
|
this.panelFrags.reset();
|
|
5450
5597
|
this.filmVerts.reset();
|
|
@@ -5456,6 +5603,7 @@ export class WebGPURenderer {
|
|
|
5456
5603
|
this.sdfTextFrags.reset();
|
|
5457
5604
|
this.lineVerts.reset();
|
|
5458
5605
|
this.lineFrags.reset();
|
|
5606
|
+
this.skyUniforms.reset();
|
|
5459
5607
|
/* The material ring, and the open slot with it: last frame's slots are gone. */
|
|
5460
5608
|
this.perFrame.reset();
|
|
5461
5609
|
this.materials.dirty();
|
|
@@ -5989,9 +6137,24 @@ export class WebGPURenderer {
|
|
|
5989
6137
|
f[base + 1] = counts[1];
|
|
5990
6138
|
f[base + 2] = counts[2];
|
|
5991
6139
|
}
|
|
6140
|
+
/* Which sets the shading reads, and how far between them. See `probeSweeps.ts`. */
|
|
6141
|
+
const setsAt = this.fragment.fields['uProbeGridSets'];
|
|
6142
|
+
if (setsAt !== undefined) {
|
|
6143
|
+
const sets = this.probeSweeps?.uniforms(this.probeSets) ?? this.probeSets.fill(0);
|
|
6144
|
+
const base = setsAt.offset / 4;
|
|
6145
|
+
f[base] = sets[0] ?? 0;
|
|
6146
|
+
f[base + 1] = sets[1] ?? 0;
|
|
6147
|
+
f[base + 2] = sets[2] ?? 0;
|
|
6148
|
+
}
|
|
5992
6149
|
/* Which chain is bound, because the level and the weight both depend on it. */
|
|
5993
6150
|
f[at('uEnvironmentPrefiltered')] = this.quality.environmentPrefilter ? 1 : 0;
|
|
5994
6151
|
}
|
|
6152
|
+
/* DriftLight, or zeros that switch it off: see `driftLight/uniforms.ts`. */
|
|
6153
|
+
this.uploadLightField();
|
|
6154
|
+
const drift = this.driftUniforms;
|
|
6155
|
+
resolveDriftLight(this.driftAtlas === null ? null : this.lightField, this.probePassActive, camera.position, drift);
|
|
6156
|
+
f.set(drift.light, at('uDriftLight'));
|
|
6157
|
+
f.set(drift.origin, at('uDriftLightOrigin'));
|
|
5995
6158
|
/*
|
|
5996
6159
|
* The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
|
|
5997
6160
|
*
|
|
@@ -6070,7 +6233,7 @@ export class WebGPURenderer {
|
|
|
6070
6233
|
* it every frame.
|
|
6071
6234
|
*/
|
|
6072
6235
|
if (this.probeEnabled) {
|
|
6073
|
-
const usable = this.
|
|
6236
|
+
const usable = this.probeReadable();
|
|
6074
6237
|
f[at('uEnvironmentEnabled')] = usable ? 1 : 0;
|
|
6075
6238
|
f[at('uEnvironmentMaxLod')] = this.probeMaxLod;
|
|
6076
6239
|
/*
|
|
@@ -6306,6 +6469,14 @@ export class WebGPURenderer {
|
|
|
6306
6469
|
}
|
|
6307
6470
|
/** Said once rather than every frame, for the reason `warnedFull` gives. */
|
|
6308
6471
|
warnedMaterialsFull = false;
|
|
6472
|
+
warnedSkyFull = false;
|
|
6473
|
+
warnedParticlesTwice = false;
|
|
6474
|
+
/**
|
|
6475
|
+
* Which encoder draws are being recorded into, by count: a new one at every `beginFrame`, and one
|
|
6476
|
+
* of its own for each probe bake, restored after. What a batch drawn per encoder is checked by.
|
|
6477
|
+
*/
|
|
6478
|
+
encoderEpoch = 0;
|
|
6479
|
+
encoderEpochs = 0;
|
|
6309
6480
|
/**
|
|
6310
6481
|
* Draw one mesh.
|
|
6311
6482
|
*
|
|
@@ -6589,7 +6760,9 @@ export class WebGPURenderer {
|
|
|
6589
6760
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
6590
6761
|
/* The two order-independent buffers are two formats and two blend states, so a draw into
|
|
6591
6762
|
one must never share a cached pipeline with the same mesh drawn into the other. */
|
|
6592
|
-
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}
|
|
6763
|
+
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
|
|
6764
|
+
/* Culling is pipeline state here: a two-sided surface culls nothing. */
|
|
6765
|
+
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
6593
6766
|
/*
|
|
6594
6767
|
* The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
|
|
6595
6768
|
* field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
|
|
@@ -6619,7 +6792,7 @@ export class WebGPURenderer {
|
|
|
6619
6792
|
if (present === undefined) {
|
|
6620
6793
|
throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
|
|
6621
6794
|
}
|
|
6622
|
-
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, blend, skinned, morphed, depthWrite, layer, false, this.oitMode);
|
|
6795
|
+
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided);
|
|
6623
6796
|
}
|
|
6624
6797
|
/*
|
|
6625
6798
|
* Guards the draw call only, not the restore below.
|
|
@@ -6825,7 +6998,8 @@ export class WebGPURenderer {
|
|
|
6825
6998
|
a third vertex buffer and a different vertex module, and sharing a cache entry with the
|
|
6826
6999
|
plain one would hand a draw the wrong layout. */
|
|
6827
7000
|
const key = `${base}|inst${blend ? '|blend' : ''}` +
|
|
6828
|
-
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}
|
|
7001
|
+
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
7002
|
+
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
6829
7003
|
const pipelines = this.targetPipelines();
|
|
6830
7004
|
let pipeline = pipelines.peek(key);
|
|
6831
7005
|
if (pipeline === undefined) {
|
|
@@ -6833,7 +7007,7 @@ export class WebGPURenderer {
|
|
|
6833
7007
|
if (present === undefined) {
|
|
6834
7008
|
throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
|
|
6835
7009
|
}
|
|
6836
|
-
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, blend, false, false, depthWrite, layer, true);
|
|
7010
|
+
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided);
|
|
6837
7011
|
}
|
|
6838
7012
|
const material = this.materialSlotForDraw();
|
|
6839
7013
|
if (material !== null) {
|
|
@@ -6974,7 +7148,10 @@ export class WebGPURenderer {
|
|
|
6974
7148
|
i[atF('uFogMode')] = medium.fogMode;
|
|
6975
7149
|
f[atF('uFogNear')] = medium.fogNear;
|
|
6976
7150
|
f[atF('uFogFar')] = medium.fogFar;
|
|
6977
|
-
this.
|
|
7151
|
+
const fragmentSlot = this.scatterFrags.allocate();
|
|
7152
|
+
if (fragmentSlot === null)
|
|
7153
|
+
return;
|
|
7154
|
+
this.scatterFrags.writeBlock(fragmentSlot, this.scatterFragInts);
|
|
6978
7155
|
const scatterPipe = scatterPipeline(this.pipelines, this.surface.device, this.scatterLayout);
|
|
6979
7156
|
/* One dynamic offset, and a group of two uniform buffers: nothing sampled, nothing read. */
|
|
6980
7157
|
const scatterCommand = this.recordDraw(0, this.currentTarget());
|
|
@@ -6982,7 +7159,8 @@ export class WebGPURenderer {
|
|
|
6982
7159
|
scatterCommand.pipeline = scatterPipe;
|
|
6983
7160
|
scatterCommand.bindGroup = this.scatterBindGroup;
|
|
6984
7161
|
scatterCommand.offsetA = slot;
|
|
6985
|
-
scatterCommand.
|
|
7162
|
+
scatterCommand.offsetB = fragmentSlot;
|
|
7163
|
+
scatterCommand.offsetCount = 2;
|
|
6986
7164
|
for (let index = 0; index < scatter.vertexBuffers.length; index++) {
|
|
6987
7165
|
scatterCommand.vertexBuffers[index] = scatter.vertexBuffers[index];
|
|
6988
7166
|
}
|
|
@@ -6997,7 +7175,7 @@ export class WebGPURenderer {
|
|
|
6997
7175
|
if (pass === null)
|
|
6998
7176
|
return;
|
|
6999
7177
|
pass.setPipeline(scatterPipe);
|
|
7000
|
-
pass.setBindGroup(0, this.scatterBindGroup, [slot]);
|
|
7178
|
+
pass.setBindGroup(0, this.scatterBindGroup, [slot, fragmentSlot]);
|
|
7001
7179
|
for (let index = 0; index < scatter.vertexBuffers.length; index++) {
|
|
7002
7180
|
pass.setVertexBuffer(index, scatter.vertexBuffers[index]);
|
|
7003
7181
|
}
|
|
@@ -7552,6 +7730,7 @@ export class WebGPURenderer {
|
|
|
7552
7730
|
*/
|
|
7553
7731
|
rebuildFlatGroupsForNewRings() {
|
|
7554
7732
|
this.flatBindGroups.clear();
|
|
7733
|
+
this.flatEnvironment = this.flatTextures('uEnvironment').view;
|
|
7555
7734
|
this.skinnedGroups.length = 0;
|
|
7556
7735
|
const albedo = this.albedo;
|
|
7557
7736
|
const normalMap = this.normalMap;
|
|
@@ -7570,6 +7749,25 @@ export class WebGPURenderer {
|
|
|
7570
7749
|
/* The open material lives in a slot of a buffer that no longer exists. */
|
|
7571
7750
|
this.materials.dirty();
|
|
7572
7751
|
}
|
|
7752
|
+
/**
|
|
7753
|
+
* The environment view the cached flat groups bind: the grid, or the blank stand-in while the
|
|
7754
|
+
* grid may not be read. What `rebindEnvironment` compares against.
|
|
7755
|
+
*/
|
|
7756
|
+
flatEnvironment = null;
|
|
7757
|
+
/**
|
|
7758
|
+
* Rebuild the flat groups if, and only if, the environment they bind has changed.
|
|
7759
|
+
*
|
|
7760
|
+
* **Every cached group holds the environment view**, so a change of it makes all of them stale;
|
|
7761
|
+
* but nothing else a bake does touches a group. Rebuilding on every bake's two edges regardless
|
|
7762
|
+
* emptied the cache twice a bake, and every material built its group again inside the bake and
|
|
7763
|
+
* again after it: a courtyard re-baking a few faces a frame spent 28% of its CPU there on the
|
|
7764
|
+
* native host, where a `createBindGroup` costs most. What would make this wrong is a second
|
|
7765
|
+
* binding that a bake changes, which would have to be compared here too.
|
|
7766
|
+
*/
|
|
7767
|
+
rebindEnvironment() {
|
|
7768
|
+
if (this.flatTextures('uEnvironment').view !== this.flatEnvironment)
|
|
7769
|
+
this.rebuildFlatBindGroup();
|
|
7770
|
+
}
|
|
7573
7771
|
/**
|
|
7574
7772
|
* Rebuild the group for the open material, and drop every cached one.
|
|
7575
7773
|
*
|
|
@@ -7581,6 +7779,7 @@ export class WebGPURenderer {
|
|
|
7581
7779
|
*/
|
|
7582
7780
|
rebuildFlatBindGroup() {
|
|
7583
7781
|
this.flatBindGroups.clear();
|
|
7782
|
+
this.flatEnvironment = this.flatTextures('uEnvironment').view;
|
|
7584
7783
|
if (this.albedo === null &&
|
|
7585
7784
|
this.normalMap === null &&
|
|
7586
7785
|
this.ormMap === null &&
|
|
@@ -7614,6 +7813,7 @@ export class WebGPURenderer {
|
|
|
7614
7813
|
drawSky(camera, sky, env) {
|
|
7615
7814
|
if (!this.canDraw())
|
|
7616
7815
|
return;
|
|
7816
|
+
this.frameSky = sky;
|
|
7617
7817
|
const f = this.skyFloats;
|
|
7618
7818
|
const at = (name) => SKY_FIELDS[name].offset / 4;
|
|
7619
7819
|
/*
|
|
@@ -7639,13 +7839,23 @@ export class WebGPURenderer {
|
|
|
7639
7839
|
const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
|
|
7640
7840
|
f.set(medium.underwaterColor, at('uUnderwaterColor'));
|
|
7641
7841
|
f[at('uUnderwaterFactor')] = medium.underwaterFactor;
|
|
7642
|
-
this.
|
|
7842
|
+
const slot = this.skyUniforms.allocate();
|
|
7843
|
+
if (slot === null) {
|
|
7844
|
+
if (!this.warnedSkyFull) {
|
|
7845
|
+
this.warnedSkyFull = true;
|
|
7846
|
+
console.warn(`WebGPU: more than ${MAX_SKY_DRAWS} sky draws in a frame; the rest are skipped`);
|
|
7847
|
+
}
|
|
7848
|
+
return;
|
|
7849
|
+
}
|
|
7850
|
+
this.skyUniforms.writeFloats(slot, 0, f);
|
|
7643
7851
|
const skyPipe = skyPipeline(this.pipelines, this.surface.device, this.skyLayout);
|
|
7644
7852
|
/* Reads nothing: the group is one uniform buffer, and the sky samples no attachment. */
|
|
7645
7853
|
const skyCommand = this.recordDraw(0, this.currentTarget());
|
|
7646
7854
|
if (skyCommand !== null) {
|
|
7647
7855
|
skyCommand.pipeline = skyPipe;
|
|
7648
7856
|
skyCommand.bindGroup = this.skyBindGroup;
|
|
7857
|
+
skyCommand.offsetA = slot;
|
|
7858
|
+
skyCommand.offsetCount = 1;
|
|
7649
7859
|
skyCommand.count = 3;
|
|
7650
7860
|
}
|
|
7651
7861
|
else {
|
|
@@ -7653,7 +7863,7 @@ export class WebGPURenderer {
|
|
|
7653
7863
|
if (pass === null)
|
|
7654
7864
|
return;
|
|
7655
7865
|
pass.setPipeline(skyPipe);
|
|
7656
|
-
pass.setBindGroup(0, this.skyBindGroup);
|
|
7866
|
+
pass.setBindGroup(0, this.skyBindGroup, [slot]);
|
|
7657
7867
|
pass.draw(3);
|
|
7658
7868
|
}
|
|
7659
7869
|
}
|
|
@@ -7765,6 +7975,18 @@ export class WebGPURenderer {
|
|
|
7765
7975
|
const bloomStrength = this.quality.bloom * this.bloomScale;
|
|
7766
7976
|
if (bloomStrength > 0)
|
|
7767
7977
|
this.runBloom(encoder);
|
|
7978
|
+
/*
|
|
7979
|
+
* Eye adaptation, metered from the scene the composite shows, and local exposure's grid beside
|
|
7980
|
+
* it. The held brightness is kept for either, since local exposure moves regions relative to it.
|
|
7981
|
+
* See `setAutoExposure` and `setLocalExposure`.
|
|
7982
|
+
*/
|
|
7983
|
+
const localising = this.localExposure > 0;
|
|
7984
|
+
const measuring = (this.autoExposure > 0 || localising) && this.exposurePass !== null;
|
|
7985
|
+
const adapting = measuring && this.autoExposure > 0;
|
|
7986
|
+
const shownScene = this.reconstructing ? this.reconShownView : this.sceneColorView;
|
|
7987
|
+
if (measuring && shownScene !== null) {
|
|
7988
|
+
this.exposurePass?.run(encoder, shownScene, this.autoExposureDt, localising);
|
|
7989
|
+
}
|
|
7768
7990
|
/* The grade travels here only where the mesh pass gave it up. See `bindMeshPass`. */
|
|
7769
7991
|
const graded = this.quality.hdrScene;
|
|
7770
7992
|
const f = this.rushFloats;
|
|
@@ -7803,6 +8025,12 @@ export class WebGPURenderer {
|
|
|
7803
8025
|
/* The grade, sampled after the curve and before the veil. See `uGradeLut` in `rush.ts`. */
|
|
7804
8026
|
f[at('uGradeStrength')] = this.gradeStrength;
|
|
7805
8027
|
f[at('uGradeSize')] = this.gradeSize;
|
|
8028
|
+
/* The lens and the print. See `setVignette` and `setFilmGrain`. */
|
|
8029
|
+
f[at('uVignette')] = this.vignette;
|
|
8030
|
+
f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
|
|
8031
|
+
f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
|
|
8032
|
+
f[at('uGrain')] = this.grain;
|
|
8033
|
+
f[at('uGrain') + 1] = this.grainSeed;
|
|
7806
8034
|
device.queue.writeBuffer(this.rushUniforms, 0, this.rushStaging);
|
|
7807
8035
|
const pass = encoder.beginRenderPass({
|
|
7808
8036
|
label: 'post.composite',
|
|
@@ -7873,6 +8101,10 @@ export class WebGPURenderer {
|
|
|
7873
8101
|
a frame that never grades anything should not carry a texture, and a declared binding
|
|
7874
8102
|
must still have a complete one — the argument the bloom fallback above makes. */
|
|
7875
8103
|
...entry(RUSH_TEXTURES.uGradeLut, this.ensureGradeView()),
|
|
8104
|
+
/* The scene as the placeholder until adaptation is asked for: the strength is zero then,
|
|
8105
|
+
and a declared binding still needs a complete texture, as the bloom fallback says. */
|
|
8106
|
+
...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
|
|
8107
|
+
...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
|
|
7876
8108
|
],
|
|
7877
8109
|
layout: this.rushLayout,
|
|
7878
8110
|
});
|
|
@@ -9582,8 +9814,13 @@ export class WebGPURenderer {
|
|
|
9582
9814
|
const swap = this.swapView();
|
|
9583
9815
|
if (swap === null)
|
|
9584
9816
|
return null;
|
|
9585
|
-
|
|
9586
|
-
|
|
9817
|
+
/*
|
|
9818
|
+
* **The drawing buffer's size, not the scene depth's.** The overlay draws on the swap image,
|
|
9819
|
+
* past the resolve, and a reconstruction draws the scene depth small: sized from that, the
|
|
9820
|
+
* device refused the pass beside a full-size swap image and every overlay went missing.
|
|
9821
|
+
*/
|
|
9822
|
+
const width = Math.max(1, this.surface.canvas.width);
|
|
9823
|
+
const height = Math.max(1, this.surface.canvas.height);
|
|
9587
9824
|
if (this.overlayDepth === null ||
|
|
9588
9825
|
this.overlayDepth.width !== width ||
|
|
9589
9826
|
this.overlayDepth.height !== height) {
|
|
@@ -9922,10 +10159,26 @@ export class WebGPURenderer {
|
|
|
9922
10159
|
* had stopped declaring. What the guard is actually for is not *building* a composer for a
|
|
9923
10160
|
* profile that never declares anything, so that is all it now guards.
|
|
9924
10161
|
*/
|
|
10162
|
+
if (this.fieldComposer === null && this.distanceFields.length === 0)
|
|
10163
|
+
return;
|
|
10164
|
+
/*
|
|
10165
|
+
* **As many cascades as reach the probe grid**, and rebuilt when the grid asks for a different
|
|
10166
|
+
* number: the far end of a courtyard longer than the default 16 m was traced against no field
|
|
10167
|
+
* at all and lit as open sky from every side. See `gi/fieldReach.ts`.
|
|
10168
|
+
*/
|
|
10169
|
+
const grid = this.indirectGrid();
|
|
10170
|
+
const cascades = grid === null
|
|
10171
|
+
? DEFAULT_FIELD_COMPOSE.cascades
|
|
10172
|
+
: cascadesForGrid(grid, DEFAULT_FIELD_COMPOSE.radius, DEFAULT_FIELD_COMPOSE.cascades, MAX_FIELD_CASCADES);
|
|
10173
|
+
if (this.fieldComposer !== null && this.fieldComposer.cascadeCount !== cascades) {
|
|
10174
|
+
this.fieldComposer.dispose();
|
|
10175
|
+
this.fieldComposer = null;
|
|
10176
|
+
}
|
|
9925
10177
|
if (this.fieldComposer === null) {
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
10178
|
+
this.fieldComposer = new FieldComposer(this.surface.device, {
|
|
10179
|
+
...DEFAULT_FIELD_COMPOSE,
|
|
10180
|
+
cascades,
|
|
10181
|
+
});
|
|
9929
10182
|
}
|
|
9930
10183
|
this.fieldComposer.compose(encoder, this.distanceFields, this.frameEye);
|
|
9931
10184
|
}
|
|
@@ -9992,11 +10245,20 @@ export class WebGPURenderer {
|
|
|
9992
10245
|
* never said what was lighting it.
|
|
9993
10246
|
*/
|
|
9994
10247
|
const env = this.frameEnv;
|
|
9995
|
-
|
|
10248
|
+
/*
|
|
10249
|
+
* And the scene's summed fixed lights, which the trace adds to what a hit receives: the bounce
|
|
10250
|
+
* of every candle, not only of the ones near enough to be shaded exactly.
|
|
10251
|
+
*/
|
|
10252
|
+
resolveDriftLight(this.bakeLightField === null ? null : this.lightField, true, this.frameEye, this.bakeDriftUniforms);
|
|
10253
|
+
/* And the frame's exact lights, flickering and moving as the frame shades them. */
|
|
10254
|
+
this.bakeLamps.count = resolveBounceLights(env, this.bounceLights);
|
|
10255
|
+
this.bakeLamps.falloff = this.lights.falloff;
|
|
10256
|
+
this.probeBaker.bake(encoder, field, grid, this.probeBaked, this.bakeLightField, this.bakeLamps, {
|
|
9996
10257
|
direction: env?.directionalDir ?? NO_SUN,
|
|
9997
10258
|
colour: env?.directionalColor ?? NO_SUN,
|
|
9998
|
-
/* What a ray that left the world finds
|
|
9999
|
-
|
|
10259
|
+
/* What a ray that left the world finds: the sky this frame drew, or its ambient. */
|
|
10260
|
+
ambient: env?.ambient ?? NO_SUN,
|
|
10261
|
+
sky: this.frameSky,
|
|
10000
10262
|
});
|
|
10001
10263
|
/*
|
|
10002
10264
|
* **The bake is what fills the layers now, so it is what marks them filled.** `probeBaked`
|
|
@@ -10187,7 +10449,7 @@ export class WebGPURenderer {
|
|
|
10187
10449
|
* currently open. Offering it earlier hands a pass a texture of undefined contents, which is
|
|
10188
10450
|
* the failure `probeBaked` was introduced for on the forward path.
|
|
10189
10451
|
*/
|
|
10190
|
-
const usable = this.probeView !== null && this.
|
|
10452
|
+
const usable = this.probeView !== null && this.probeReadable();
|
|
10191
10453
|
if (usable) {
|
|
10192
10454
|
const environment = this.passEnvironment;
|
|
10193
10455
|
environment.view = this.probeView;
|
|
@@ -10627,6 +10889,8 @@ export class WebGPURenderer {
|
|
|
10627
10889
|
* -----------------------------------------------------------------------------------------
|
|
10628
10890
|
*/
|
|
10629
10891
|
bloomScale = 1;
|
|
10892
|
+
/** In scene units, once `setBloom` has moved it; null reads the profile's. */
|
|
10893
|
+
bloomThresholdSet = null;
|
|
10630
10894
|
rushStrength = 0;
|
|
10631
10895
|
motionBlurScale = 1;
|
|
10632
10896
|
/**
|
|
@@ -10638,6 +10902,48 @@ export class WebGPURenderer {
|
|
|
10638
10902
|
*/
|
|
10639
10903
|
veilColor = [0, 0, 0];
|
|
10640
10904
|
veilAlpha = 0;
|
|
10905
|
+
vignette = 0;
|
|
10906
|
+
grain = 0;
|
|
10907
|
+
grainSeed = 0;
|
|
10908
|
+
warnedLookWithoutComposite = false;
|
|
10909
|
+
/** Eye adaptation: how far, this frame's time step, and the pass once asked for. */
|
|
10910
|
+
autoExposure = 0;
|
|
10911
|
+
autoExposureDt = 0;
|
|
10912
|
+
/** How far each region is brought toward the frame. See `setLocalExposure`. */
|
|
10913
|
+
localExposure = 0;
|
|
10914
|
+
exposurePass = null;
|
|
10915
|
+
/**
|
|
10916
|
+
* How strongly the lens darkens the frame's corners, held until changed: 0 is none, 0.5 is about
|
|
10917
|
+
* 1.2 stops at the corner. Applied to scene light before the tone curve, as a lens loses light;
|
|
10918
|
+
* see `filmLook.ts`. Needs `screenEffects`, and says so once rather than doing nothing.
|
|
10919
|
+
*/
|
|
10920
|
+
setVignette(strength) {
|
|
10921
|
+
if (!this.hasComposite) {
|
|
10922
|
+
this.warnLookWithoutComposite(strength);
|
|
10923
|
+
return;
|
|
10924
|
+
}
|
|
10925
|
+
this.vignette = clampVignette(strength);
|
|
10926
|
+
}
|
|
10927
|
+
/**
|
|
10928
|
+
* Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
|
|
10929
|
+
* grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
|
|
10930
|
+
* its caller: pass a new one each frame for grain that moves, and the same one for a still that
|
|
10931
|
+
* is identical run to run. Applied last, after the grade and the veil; see `filmLook.ts`.
|
|
10932
|
+
*/
|
|
10933
|
+
setFilmGrain(strength, seed) {
|
|
10934
|
+
if (!this.hasComposite) {
|
|
10935
|
+
this.warnLookWithoutComposite(strength);
|
|
10936
|
+
return;
|
|
10937
|
+
}
|
|
10938
|
+
this.grain = clampGrain(strength);
|
|
10939
|
+
this.grainSeed = grainSeed(seed);
|
|
10940
|
+
}
|
|
10941
|
+
warnLookWithoutComposite(strength) {
|
|
10942
|
+
if (strength > 0 && !this.warnedLookWithoutComposite) {
|
|
10943
|
+
this.warnedLookWithoutComposite = true;
|
|
10944
|
+
console.warn(FILM_LOOK_WITHOUT_COMPOSITE);
|
|
10945
|
+
}
|
|
10946
|
+
}
|
|
10641
10947
|
/*
|
|
10642
10948
|
* **These three warned that the composite did not exist, and went on warning after it did.**
|
|
10643
10949
|
*
|
|
@@ -10713,8 +11019,17 @@ export class WebGPURenderer {
|
|
|
10713
11019
|
this.gradeView = this.gradeTexture.createView({ dimension: '3d' });
|
|
10714
11020
|
this.gradeSize = lut.size;
|
|
10715
11021
|
}
|
|
10716
|
-
setBloom(scale) {
|
|
11022
|
+
setBloom(scale, threshold) {
|
|
10717
11023
|
this.bloomScale = Math.min(Math.max(scale, 0), 1);
|
|
11024
|
+
if (threshold === undefined)
|
|
11025
|
+
return;
|
|
11026
|
+
const next = bloomThresholdOf(threshold);
|
|
11027
|
+
if (next === this.bloomThresholdSet)
|
|
11028
|
+
return;
|
|
11029
|
+
this.bloomThresholdSet = next;
|
|
11030
|
+
/* The blocks are written once per size, so a moved threshold rewrites them now. */
|
|
11031
|
+
if (this.bloomLevels.length > 0)
|
|
11032
|
+
this.writeBloomBlocks();
|
|
10718
11033
|
}
|
|
10719
11034
|
/**
|
|
10720
11035
|
* How thick the air is, this frame: a global participating medium filling the whole frustum.
|
|
@@ -10739,6 +11054,62 @@ export class WebGPURenderer {
|
|
|
10739
11054
|
setCameraMotionBlur(scale) {
|
|
10740
11055
|
this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
|
|
10741
11056
|
}
|
|
11057
|
+
/**
|
|
11058
|
+
* The camera has cut: this frame is a new shot, not the next moment of the last one.
|
|
11059
|
+
*
|
|
11060
|
+
* **Everything temporal reprojects through the previous frame's view**: the motion blur measures
|
|
11061
|
+
* its smear against it, and the temporal resolve reads its history through it. A cut is a jump
|
|
11062
|
+
* the renderer cannot tell from a fast camera, so without this a transport that seeks, a respawn
|
|
11063
|
+
* or an edit smears its first frame along the whole jump and blends in a picture of somewhere
|
|
11064
|
+
* else. The caller knows it cut; this is how it says so, before the frame's `beginFrame`.
|
|
11065
|
+
*
|
|
11066
|
+
* What it gives up is one frame of history: the frame after a cut has no blur and no temporal
|
|
11067
|
+
* blend, as the first frame of a session has none. Nothing else is reset.
|
|
11068
|
+
*/
|
|
11069
|
+
cameraCut() {
|
|
11070
|
+
this.hasPreviousView = false;
|
|
11071
|
+
this.temporalHistory.invalidate();
|
|
11072
|
+
/* The reconstruction keeps histories of its own, and they are the last shot's too. */
|
|
11073
|
+
this.reconHasHistory = false;
|
|
11074
|
+
/* And the eye was adapted to the last shot. */
|
|
11075
|
+
this.exposurePass?.cut();
|
|
11076
|
+
}
|
|
11077
|
+
/**
|
|
11078
|
+
* Eye adaptation. **The other backend's `setAutoExposure` carries the reasoning**, whole; this is
|
|
11079
|
+
* the same setter over the same state, and the pass is built the first time it is asked for, so
|
|
11080
|
+
* a renderer that never adapts carries none of it.
|
|
11081
|
+
*/
|
|
11082
|
+
setAutoExposure(strength, dtSec) {
|
|
11083
|
+
if (!this.hasComposite || !this.quality.hdrScene) {
|
|
11084
|
+
this.warnLookWithoutComposite(strength);
|
|
11085
|
+
return;
|
|
11086
|
+
}
|
|
11087
|
+
this.autoExposure = clampAutoExposure(strength);
|
|
11088
|
+
this.autoExposureDt = dtSec;
|
|
11089
|
+
if (this.autoExposure > 0)
|
|
11090
|
+
this.ensureExposurePass();
|
|
11091
|
+
}
|
|
11092
|
+
/**
|
|
11093
|
+
* Local exposure. **The other backend's `setLocalExposure` carries the reasoning**, whole; this is
|
|
11094
|
+
* the same setter over the same state, sharing eye adaptation's pass, which is built the first
|
|
11095
|
+
* time either is asked for.
|
|
11096
|
+
*/
|
|
11097
|
+
setLocalExposure(strength) {
|
|
11098
|
+
if (!this.hasComposite || !this.quality.hdrScene) {
|
|
11099
|
+
this.warnLookWithoutComposite(strength);
|
|
11100
|
+
return;
|
|
11101
|
+
}
|
|
11102
|
+
this.localExposure = clampLocalExposure(strength);
|
|
11103
|
+
if (this.localExposure > 0)
|
|
11104
|
+
this.ensureExposurePass();
|
|
11105
|
+
}
|
|
11106
|
+
/** The exposure pass, built once; the composite's group holds placeholders until it exists. */
|
|
11107
|
+
ensureExposurePass() {
|
|
11108
|
+
if (this.exposurePass !== null)
|
|
11109
|
+
return;
|
|
11110
|
+
this.exposurePass = new ExposurePass(this.surface.device, this.pipelines, this.postSampler);
|
|
11111
|
+
this.rebuildRushBindGroup();
|
|
11112
|
+
}
|
|
10742
11113
|
/**
|
|
10743
11114
|
* Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
|
|
10744
11115
|
* to take. **The other backend's `setDepthOfField` carries the reasoning**, whole: the
|
|
@@ -10943,6 +11314,8 @@ export class WebGPURenderer {
|
|
|
10943
11314
|
* over a lobe-convolved one.
|
|
10944
11315
|
*/
|
|
10945
11316
|
probe;
|
|
11317
|
+
/** The views, groups and attachments a bake draws with, made once. See `probeTargets.ts`. */
|
|
11318
|
+
probeTargets;
|
|
10946
11319
|
/**
|
|
10947
11320
|
* The prefiltered cube: what the lit pass samples, one GGX-convolved level per roughness.
|
|
10948
11321
|
*
|
|
@@ -10974,6 +11347,19 @@ export class WebGPURenderer {
|
|
|
10974
11347
|
probeMipLayout;
|
|
10975
11348
|
/** The photometric atlas, allocated on first use. See `setIesProfiles`. */
|
|
10976
11349
|
iesTexture = null;
|
|
11350
|
+
/** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
|
|
11351
|
+
lightField = null;
|
|
11352
|
+
driftIndex = null;
|
|
11353
|
+
driftAtlas = null;
|
|
11354
|
+
driftIndexView = null;
|
|
11355
|
+
driftAtlasView = null;
|
|
11356
|
+
driftUniforms = createDriftLightUniforms();
|
|
11357
|
+
/** The same field resolved for a traced probe, and what the baker binds; null while not whole. */
|
|
11358
|
+
bakeDriftUniforms = createDriftLightUniforms();
|
|
11359
|
+
bakeLightField = null;
|
|
11360
|
+
/** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
|
|
11361
|
+
bounceLights = createBounceLights();
|
|
11362
|
+
bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
|
|
10977
11363
|
iesView = null;
|
|
10978
11364
|
cookieTexture = null;
|
|
10979
11365
|
cookieView = null;
|
|
@@ -10998,8 +11384,9 @@ export class WebGPURenderer {
|
|
|
10998
11384
|
* mirroring uninitialised memory is worse than a car mirroring a gradient — the rule one probe
|
|
10999
11385
|
* already had, unchanged by there being more of them.
|
|
11000
11386
|
*/
|
|
11001
|
-
|
|
11002
|
-
|
|
11387
|
+
probeSweeps = null;
|
|
11388
|
+
/** `uProbeGridSets`, rewritten in place. */
|
|
11389
|
+
probeSets = new Float32Array(3);
|
|
11003
11390
|
/**
|
|
11004
11391
|
* Whether a bake is running, which is when the cube may be neither sampled nor bound.
|
|
11005
11392
|
*
|
|
@@ -11007,13 +11394,23 @@ export class WebGPURenderer {
|
|
|
11007
11394
|
* load-bearing rather than defensive: WebGPU rejects a pass that holds the texture it writes.
|
|
11008
11395
|
*/
|
|
11009
11396
|
probePassActive = false;
|
|
11397
|
+
/** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
|
|
11398
|
+
probeBounce = false;
|
|
11399
|
+
/**
|
|
11400
|
+
* Whether the shading may read the grid now: once every probe is filled, and not during a bake
|
|
11401
|
+
* unless it is a bounce bake. A bounce bake's faces are drawn into the probe's own cube and only
|
|
11402
|
+
* the resolve writes the array, in a pass of its own, so no pass both samples and writes it.
|
|
11403
|
+
*/
|
|
11404
|
+
probeReadable() {
|
|
11405
|
+
return this.probeBaked && (!this.probePassActive || this.probeBounce);
|
|
11406
|
+
}
|
|
11010
11407
|
/** How many mip levels the roughness blur has to work with. `flat.ts` reads this. */
|
|
11011
11408
|
get probeMaxLod() {
|
|
11012
11409
|
return ggxMaxLevelFor(this.probeEdge);
|
|
11013
11410
|
}
|
|
11014
|
-
/** Whether
|
|
11411
|
+
/** Whether a whole sweep of the grid has been convolved. See `probeSweeps.ts`. */
|
|
11015
11412
|
get probeBaked() {
|
|
11016
|
-
return this.probes !== null && this.
|
|
11413
|
+
return this.probes !== null && this.probeSweeps?.ready === true;
|
|
11017
11414
|
}
|
|
11018
11415
|
/**
|
|
11019
11416
|
* Record that one layer now holds a convolution, without counting a rebake twice.
|
|
@@ -11032,13 +11429,9 @@ export class WebGPURenderer {
|
|
|
11032
11429
|
* mistake, and `setProbeGrid` says it about the array being replaced.
|
|
11033
11430
|
*/
|
|
11034
11431
|
markProbeFilled(layer) {
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
if (this.
|
|
11038
|
-
return;
|
|
11039
|
-
this.probeFilled[layer] = 1;
|
|
11040
|
-
this.probeFilledCount++;
|
|
11041
|
-
if (this.probeBaked)
|
|
11432
|
+
const was = this.probeBaked;
|
|
11433
|
+
this.probeSweeps?.baked(layer);
|
|
11434
|
+
if (!was && this.probeBaked)
|
|
11042
11435
|
this.rebuildFlatGroupsForNewRings();
|
|
11043
11436
|
}
|
|
11044
11437
|
/**
|
|
@@ -11049,6 +11442,7 @@ export class WebGPURenderer {
|
|
|
11049
11442
|
*/
|
|
11050
11443
|
allocateProbeArray(layers, device) {
|
|
11051
11444
|
this.probeArray?.destroy();
|
|
11445
|
+
this.probeTargets?.forgetArray();
|
|
11052
11446
|
this.probeArray = device.createTexture({
|
|
11053
11447
|
label: 'probe.array',
|
|
11054
11448
|
size: [this.probeEdge, this.probeEdge, layers],
|
|
@@ -11058,8 +11452,6 @@ export class WebGPURenderer {
|
|
|
11058
11452
|
usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
|
|
11059
11453
|
});
|
|
11060
11454
|
this.probeView = this.probeArray.createView({ dimension: '2d-array' });
|
|
11061
|
-
this.probeFilled = new Uint8Array(layers);
|
|
11062
|
-
this.probeFilledCount = 0;
|
|
11063
11455
|
/*
|
|
11064
11456
|
* **Cleared here, because a texture no pass has written holds undefined contents** — the same
|
|
11065
11457
|
* sentence the directional shadow maps are cleared under, arrived at from the other end.
|
|
@@ -11116,8 +11508,14 @@ export class WebGPURenderer {
|
|
|
11116
11508
|
if (current !== null && this.probeArray !== null && sameGrid(current, grid))
|
|
11117
11509
|
return true;
|
|
11118
11510
|
this.probes = grid;
|
|
11119
|
-
|
|
11120
|
-
|
|
11511
|
+
/* A traced grid writes its layers itself, so it keeps one set. See `ProbeGridOptions.crossfade`. */
|
|
11512
|
+
const crossfade = grid.crossfade && !this.quality.indirectLight;
|
|
11513
|
+
const sets = crossfade ? 3 : 1;
|
|
11514
|
+
if (this.probeArray === null ||
|
|
11515
|
+
this.probeArray.depthOrArrayLayers !== grid.layers * sets ||
|
|
11516
|
+
this.probeSweeps?.sets !== sets) {
|
|
11517
|
+
this.probeSweeps = new ProbeSweeps(grid.layers, crossfade);
|
|
11518
|
+
this.allocateProbeArray(grid.layers * sets, this.surface.device);
|
|
11121
11519
|
this.rebuildFlatGroupsForNewRings();
|
|
11122
11520
|
}
|
|
11123
11521
|
return true;
|
|
@@ -11179,39 +11577,18 @@ export class WebGPURenderer {
|
|
|
11179
11577
|
camera.far = PROBE_FAR_M;
|
|
11180
11578
|
camera.roll = 0;
|
|
11181
11579
|
const { device } = this.surface;
|
|
11182
|
-
const
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
format: DEPTH_FORMAT,
|
|
11186
|
-
/*
|
|
11187
|
-
* **The world's sample count, because a bake draws the world with the world's pipelines.**
|
|
11188
|
-
* A pipeline whose count disagrees with its attachment is rejected at `finish` and takes
|
|
11189
|
-
* the whole bake with it, and WebGPU requires every attachment in a pass to agree — so a
|
|
11190
|
-
* single-sample depth beside a four-sample colour is refused in words that read as a depth
|
|
11191
|
-
* problem and are a multisampling one.
|
|
11192
|
-
*/
|
|
11193
|
-
sampleCount: this.samples,
|
|
11194
|
-
usage: 0x10, // RENDER_ATTACHMENT
|
|
11195
|
-
});
|
|
11196
|
-
const depthView = depth.createView();
|
|
11580
|
+
const targets = this.probeTargets;
|
|
11581
|
+
if (targets === null)
|
|
11582
|
+
return false;
|
|
11197
11583
|
/*
|
|
11198
|
-
* The
|
|
11199
|
-
*
|
|
11200
|
-
*
|
|
11201
|
-
*
|
|
11202
|
-
*
|
|
11203
|
-
*
|
|
11584
|
+
* The depth, at the world's sample count because a bake draws the world with the world's
|
|
11585
|
+
* pipelines: a pipeline whose count disagrees with its attachment is rejected at `finish` and
|
|
11586
|
+
* takes the whole bake with it, in words that read as a depth problem and are a multisampling
|
|
11587
|
+
* one. And the multisampled twin the faces are drawn into and resolved from, since **a cube
|
|
11588
|
+
* face cannot be both multisampled and sampled** — the same bind the mirror is in. Both kept
|
|
11589
|
+
* between bakes: see `probeTargets.ts`.
|
|
11204
11590
|
*/
|
|
11205
|
-
const colorMsaa = this.samples
|
|
11206
|
-
? device.createTexture({
|
|
11207
|
-
label: 'probe.colorMsaa',
|
|
11208
|
-
size: [this.probeSize, this.probeSize],
|
|
11209
|
-
format: this.pipelines.format,
|
|
11210
|
-
sampleCount: this.samples,
|
|
11211
|
-
usage: 0x10, // RENDER_ATTACHMENT
|
|
11212
|
-
})
|
|
11213
|
-
: null;
|
|
11214
|
-
const colorMsaaView = colorMsaa?.createView() ?? null;
|
|
11591
|
+
const { depth: depthView, colorMsaa: colorMsaaView } = targets.attachments(this.samples, this.pipelines.format);
|
|
11215
11592
|
/*
|
|
11216
11593
|
* The frame's own pass is set aside and restored, because `drawFace` calls straight back into
|
|
11217
11594
|
* `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
|
|
@@ -11230,16 +11607,26 @@ export class WebGPURenderer {
|
|
|
11230
11607
|
if (this.quality.frameGraph)
|
|
11231
11608
|
this.flushGraph();
|
|
11232
11609
|
const savedEncoder = this.encoder;
|
|
11610
|
+
/*
|
|
11611
|
+
* **The rings are marked here and given back after the submit below**, because this bake is its
|
|
11612
|
+
* own encoder: once it is submitted nothing in the frame can read its slots, and a grid baked in
|
|
11613
|
+
* one call would otherwise spend the frame's whole ring on probes and skip the draws past it.
|
|
11614
|
+
*/
|
|
11615
|
+
this.markRings();
|
|
11616
|
+
const skinMark = this.skinPalettes.mark();
|
|
11617
|
+
const frameEpoch = this.encoderEpoch;
|
|
11618
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
11233
11619
|
const encoder = device.createCommandEncoder({ label: 'probe' });
|
|
11234
11620
|
this.encoder = encoder;
|
|
11235
11621
|
/*
|
|
11236
|
-
* Fenced before the first face and released after the last,
|
|
11237
|
-
*
|
|
11238
|
-
*
|
|
11239
|
-
*
|
|
11622
|
+
* Fenced before the first face and released after the last, so the grid is not bound while a
|
|
11623
|
+
* plain bake is drawn. **Rebuilt only where that changes what the group binds**: a bounce bake
|
|
11624
|
+
* of a whole grid binds it exactly as the frame does, and it is the bake a scene repeats every
|
|
11625
|
+
* frame, a few faces at a time. See `rebindEnvironment`.
|
|
11240
11626
|
*/
|
|
11627
|
+
this.probeBounce = options?.bounce ?? false;
|
|
11241
11628
|
this.probePassActive = true;
|
|
11242
|
-
this.
|
|
11629
|
+
this.rebindEnvironment();
|
|
11243
11630
|
/*
|
|
11244
11631
|
* **A probe stores radiance, not display pixels.** See `renderer.ts` for the whole argument:
|
|
11245
11632
|
* baking through the output transform crushes every highlight with the tone curve before it
|
|
@@ -11251,7 +11638,9 @@ export class WebGPURenderer {
|
|
|
11251
11638
|
this.perFrameInts[this.materialField('uOutputTransform')] = 0;
|
|
11252
11639
|
this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
|
|
11253
11640
|
this.materials.dirty();
|
|
11254
|
-
|
|
11641
|
+
/* Only the faces asked for; the chain waits for the sixth. See `ProbeBakeOptions.faces`. */
|
|
11642
|
+
const [firstFace, endFace] = probeFaceRange(options);
|
|
11643
|
+
for (let face = firstFace; face < endFace; face++) {
|
|
11255
11644
|
const aim = PROBE_FACES[face];
|
|
11256
11645
|
if (aim === undefined)
|
|
11257
11646
|
continue;
|
|
@@ -11262,13 +11651,7 @@ export class WebGPURenderer {
|
|
|
11262
11651
|
* face and shows up as a reflection that slides at the wrong rate across a curved surface.
|
|
11263
11652
|
*/
|
|
11264
11653
|
camera.updateMatrices(1);
|
|
11265
|
-
const faceView =
|
|
11266
|
-
dimension: '2d',
|
|
11267
|
-
baseArrayLayer: face,
|
|
11268
|
-
arrayLayerCount: 1,
|
|
11269
|
-
baseMipLevel: 0,
|
|
11270
|
-
mipLevelCount: 1,
|
|
11271
|
-
});
|
|
11654
|
+
const faceView = targets.faceViews[face];
|
|
11272
11655
|
this.pass = encoder.beginRenderPass({
|
|
11273
11656
|
label: `probe.face${face}`,
|
|
11274
11657
|
colorAttachments: [
|
|
@@ -11314,19 +11697,23 @@ export class WebGPURenderer {
|
|
|
11314
11697
|
this.pass.end();
|
|
11315
11698
|
}
|
|
11316
11699
|
this.pass = null;
|
|
11317
|
-
|
|
11700
|
+
const whole = endFace === PROBE_FACES.length;
|
|
11701
|
+
if (whole)
|
|
11702
|
+
this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
|
|
11318
11703
|
this.flushRings();
|
|
11319
11704
|
device.queue.submit([encoder.finish()]);
|
|
11320
|
-
|
|
11321
|
-
|
|
11705
|
+
this.rewindRings(skinMark);
|
|
11706
|
+
this.encoderEpoch = frameEpoch;
|
|
11322
11707
|
this.encoder = savedEncoder;
|
|
11323
11708
|
this.pass = savedPass;
|
|
11324
|
-
|
|
11709
|
+
if (whole)
|
|
11710
|
+
this.markProbeFilled(layer);
|
|
11325
11711
|
this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
|
|
11326
11712
|
this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
|
|
11327
11713
|
this.materials.dirty();
|
|
11328
11714
|
this.probePassActive = false;
|
|
11329
|
-
this.
|
|
11715
|
+
this.probeBounce = false;
|
|
11716
|
+
this.rebindEnvironment();
|
|
11330
11717
|
return true;
|
|
11331
11718
|
}
|
|
11332
11719
|
/**
|
|
@@ -11453,6 +11840,77 @@ export class WebGPURenderer {
|
|
|
11453
11840
|
this.cookieTiles = tiles;
|
|
11454
11841
|
this.rebuildFlatBindGroup();
|
|
11455
11842
|
}
|
|
11843
|
+
/**
|
|
11844
|
+
* Sum a scene's many fixed lights into a DriftLight field. The same contract as
|
|
11845
|
+
* `Renderer.createLightField`; see `driftLight/lightField.ts`.
|
|
11846
|
+
*/
|
|
11847
|
+
createLightField(lights, options = {}) {
|
|
11848
|
+
this.forgetLightFieldVolumes();
|
|
11849
|
+
const field = new LightField(lights, { ...options, falloff: this.quality.pointLightFalloff });
|
|
11850
|
+
this.lightField = field;
|
|
11851
|
+
return field;
|
|
11852
|
+
}
|
|
11853
|
+
/** Let go of the field and its volumes; the scene's lights are all exact again. */
|
|
11854
|
+
disposeLightField() {
|
|
11855
|
+
this.forgetLightFieldVolumes();
|
|
11856
|
+
this.lightField = null;
|
|
11857
|
+
}
|
|
11858
|
+
forgetLightFieldVolumes() {
|
|
11859
|
+
const bound = this.driftAtlas !== null;
|
|
11860
|
+
this.driftIndex?.destroy();
|
|
11861
|
+
this.driftAtlas?.destroy();
|
|
11862
|
+
this.driftIndex = null;
|
|
11863
|
+
this.driftAtlas = null;
|
|
11864
|
+
this.driftIndexView = null;
|
|
11865
|
+
this.driftAtlasView = null;
|
|
11866
|
+
this.bakeLightField = null;
|
|
11867
|
+
/* The groups hold the destroyed volumes, and a group holding a destroyed texture fails. */
|
|
11868
|
+
if (bound && !this.surface.lost)
|
|
11869
|
+
this.rebuildFlatBindGroup();
|
|
11870
|
+
}
|
|
11871
|
+
/**
|
|
11872
|
+
* Upload the field's two volumes, once, when its last brick has baked, and rebuild the groups
|
|
11873
|
+
* that bind them: nothing samples them before that, so uploading as bricks land would be work
|
|
11874
|
+
* for no picture, and a rebuild per brick would be a bind group per brick.
|
|
11875
|
+
*/
|
|
11876
|
+
uploadLightField() {
|
|
11877
|
+
const field = this.lightField;
|
|
11878
|
+
if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
|
|
11879
|
+
return;
|
|
11880
|
+
}
|
|
11881
|
+
if (this.surface.lost)
|
|
11882
|
+
return;
|
|
11883
|
+
const { device } = this.surface;
|
|
11884
|
+
const [ix, iy, iz] = field.layout.dims;
|
|
11885
|
+
const index = device.createTexture({
|
|
11886
|
+
label: 'driftLight.index',
|
|
11887
|
+
size: [ix, iy, iz],
|
|
11888
|
+
dimension: '3d',
|
|
11889
|
+
format: 'r32uint',
|
|
11890
|
+
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
11891
|
+
});
|
|
11892
|
+
device.queue.writeTexture({ texture: index }, field.layout.index, { bytesPerRow: ix * 4, rowsPerImage: iy }, [ix, iy, iz]);
|
|
11893
|
+
const [aw, ah, ad] = field.atlasSize;
|
|
11894
|
+
const atlas = device.createTexture({
|
|
11895
|
+
label: 'driftLight.atlas',
|
|
11896
|
+
size: [aw, ah, ad],
|
|
11897
|
+
dimension: '3d',
|
|
11898
|
+
format: 'rgba16float',
|
|
11899
|
+
usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
|
|
11900
|
+
});
|
|
11901
|
+
device.queue.writeTexture({ texture: atlas }, field.atlas, { bytesPerRow: aw * 8, rowsPerImage: ah }, [aw, ah, ad]);
|
|
11902
|
+
this.driftIndex = index;
|
|
11903
|
+
this.driftAtlas = atlas;
|
|
11904
|
+
this.driftIndexView = index.createView({ dimension: '3d' });
|
|
11905
|
+
this.driftAtlasView = atlas.createView({ dimension: '3d' });
|
|
11906
|
+
this.bakeLightField = {
|
|
11907
|
+
index: this.driftIndexView,
|
|
11908
|
+
atlas: this.driftAtlasView,
|
|
11909
|
+
light: this.bakeDriftUniforms.light,
|
|
11910
|
+
origin: this.bakeDriftUniforms.origin,
|
|
11911
|
+
};
|
|
11912
|
+
this.rebuildFlatBindGroup();
|
|
11913
|
+
}
|
|
11456
11914
|
setIesProfiles(profiles) {
|
|
11457
11915
|
if (this.surface.lost)
|
|
11458
11916
|
return;
|
|
@@ -11496,6 +11954,9 @@ export class WebGPURenderer {
|
|
|
11496
11954
|
/** Each level from the one above it, six faces at a time. See `probePass.ts`. */
|
|
11497
11955
|
buildProbeChain(encoder, probe, layer) {
|
|
11498
11956
|
const { device } = this.surface;
|
|
11957
|
+
const targets = this.probeTargets;
|
|
11958
|
+
if (targets === null)
|
|
11959
|
+
return;
|
|
11499
11960
|
/*
|
|
11500
11961
|
* **Reset and flushed here, because a bake is not a frame.**
|
|
11501
11962
|
*
|
|
@@ -11519,12 +11980,8 @@ export class WebGPURenderer {
|
|
|
11519
11980
|
const pipeline = probeMipPipeline(this.pipelines, device, this.probeMipLayout, this.pipelines.format);
|
|
11520
11981
|
const levels = probeLevels(this.probeSize);
|
|
11521
11982
|
for (let level = 1; level < levels; level++) {
|
|
11522
|
-
/*
|
|
11523
|
-
const
|
|
11524
|
-
dimension: 'cube',
|
|
11525
|
-
baseMipLevel: level - 1,
|
|
11526
|
-
mipLevelCount: 1,
|
|
11527
|
-
});
|
|
11983
|
+
/* The whole cube at the level above, so the blur can cross a face edge, in a group kept. */
|
|
11984
|
+
const group = targets.chainGroup(this.probeMipLayout, this.probeMipUniforms.buffer, this.probeSampler, level);
|
|
11528
11985
|
for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
|
|
11529
11986
|
const basis = PROBE_FACE_BASIS[face];
|
|
11530
11987
|
if (basis === undefined)
|
|
@@ -11540,21 +11997,15 @@ export class WebGPURenderer {
|
|
|
11540
11997
|
this.probeMipUniforms.flush();
|
|
11541
11998
|
return;
|
|
11542
11999
|
}
|
|
11543
|
-
this.probeMipUniforms.writeFloats(slot, 0,
|
|
11544
|
-
this.probeMipUniforms.writeFloats(slot, 16,
|
|
11545
|
-
this.probeMipUniforms.writeFloats(slot, 32,
|
|
11546
|
-
this.probeMipUniforms.writeFloats(slot, 48,
|
|
12000
|
+
this.probeMipUniforms.writeFloats(slot, 0, padded(basis.basisX));
|
|
12001
|
+
this.probeMipUniforms.writeFloats(slot, 16, padded(basis.basisY));
|
|
12002
|
+
this.probeMipUniforms.writeFloats(slot, 32, padded(basis.forward));
|
|
12003
|
+
this.probeMipUniforms.writeFloats(slot, 48, ZERO4);
|
|
11547
12004
|
const pass = encoder.beginRenderPass({
|
|
11548
12005
|
label: `probe.mip${level}.face${face}`,
|
|
11549
12006
|
colorAttachments: [
|
|
11550
12007
|
{
|
|
11551
|
-
view:
|
|
11552
|
-
dimension: '2d',
|
|
11553
|
-
baseArrayLayer: face,
|
|
11554
|
-
arrayLayerCount: 1,
|
|
11555
|
-
baseMipLevel: level,
|
|
11556
|
-
mipLevelCount: 1,
|
|
11557
|
-
}),
|
|
12008
|
+
view: targets.chainTarget(level, face),
|
|
11558
12009
|
loadOp: 'clear',
|
|
11559
12010
|
storeOp: 'store',
|
|
11560
12011
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -11562,17 +12013,7 @@ export class WebGPURenderer {
|
|
|
11562
12013
|
],
|
|
11563
12014
|
});
|
|
11564
12015
|
pass.setPipeline(pipeline);
|
|
11565
|
-
pass.setBindGroup(0,
|
|
11566
|
-
layout: this.probeMipLayout,
|
|
11567
|
-
entries: [
|
|
11568
|
-
{
|
|
11569
|
-
binding: 0,
|
|
11570
|
-
resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
|
|
11571
|
-
},
|
|
11572
|
-
{ binding: 1, resource: source },
|
|
11573
|
-
{ binding: 2, resource: this.probeSampler },
|
|
11574
|
-
],
|
|
11575
|
-
}), [slot]);
|
|
12016
|
+
pass.setBindGroup(0, group, [slot]);
|
|
11576
12017
|
pass.draw(3);
|
|
11577
12018
|
pass.end();
|
|
11578
12019
|
}
|
|
@@ -11593,8 +12034,8 @@ export class WebGPURenderer {
|
|
|
11593
12034
|
const prefilterTarget = this.probeArray;
|
|
11594
12035
|
if (prefilterTarget !== null) {
|
|
11595
12036
|
const prefilter = probePrefilterPipeline(this.pipelines, device, this.probePrefilterLayout, this.pipelines.format);
|
|
11596
|
-
/* The whole chain, so a wide sample can select a level from it. */
|
|
11597
|
-
const
|
|
12037
|
+
/* The whole chain, so a wide sample can select a level from it, in a group kept. */
|
|
12038
|
+
const group = targets.prefilterGroupFor(this.probePrefilterLayout, this.probeMipUniforms.buffer, this.probeSampler);
|
|
11598
12039
|
const offsets = PREFILTER_UNIFORM_OFFSETS;
|
|
11599
12040
|
const topLevel = levels - 1;
|
|
11600
12041
|
const samples = this.quality.environmentPrefilterSamples;
|
|
@@ -11602,12 +12043,18 @@ export class WebGPURenderer {
|
|
|
11602
12043
|
const irradianceLevel = irradianceLevelFor(edge);
|
|
11603
12044
|
const ggxTop = ggxMaxLevelFor(edge);
|
|
11604
12045
|
const boxChain = !this.quality.environmentPrefilter;
|
|
12046
|
+
/*
|
|
12047
|
+
* **The diffuse level of a layer the trace has written is the trace's**, so a rasterised bake
|
|
12048
|
+
* stops one level short of it. See `ProbeBaker.traced`: two writers of one level was light
|
|
12049
|
+
* sweeping down a courtyard a probe at a time with the clock paused.
|
|
12050
|
+
*/
|
|
12051
|
+
const lastLevel = this.probeBaker?.traced(layer) === true ? irradianceLevel - 1 : irradianceLevel;
|
|
11605
12052
|
/*
|
|
11606
12053
|
* **One draw a level, where the cube needed six.** An octahedral map has no faces, so the
|
|
11607
12054
|
* six-way loop is gone rather than moved, and a probe's convolution is six times fewer
|
|
11608
12055
|
* passes than the cube it replaces.
|
|
11609
12056
|
*/
|
|
11610
|
-
for (let level = 0; level <=
|
|
12057
|
+
for (let level = 0; level <= lastLevel; level++) {
|
|
11611
12058
|
const irradiance = level === irradianceLevel;
|
|
11612
12059
|
const roughness = irradiance ? 1 : roughnessForLevel(level, ggxTop);
|
|
11613
12060
|
const slot = this.probeMipUniforms.allocate();
|
|
@@ -11624,31 +12071,19 @@ export class WebGPURenderer {
|
|
|
11624
12071
|
/* Written at the generator's own offsets rather than at literals, because a roughness at
|
|
11625
12072
|
the wrong offset is a level convolved for some other roughness — which looks like a
|
|
11626
12073
|
plausible reflection and points at nothing. */
|
|
11627
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterEdge.offset,
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
this.probeMipUniforms.writeFloats(slot, offsets.
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
this.probeMipUniforms.writeFloats(slot, offsets.
|
|
11634
|
-
this.probeMipUniforms.writeFloats(slot, offsets.
|
|
11635
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterRoughness.offset, [roughness]);
|
|
11636
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSamples.offset, [samples]);
|
|
11637
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceTexels.offset, [
|
|
11638
|
-
this.probeSize,
|
|
11639
|
-
]);
|
|
11640
|
-
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceMaxLod.offset, [topLevel]);
|
|
12074
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterEdge.offset, one(Math.max(1, edge >> level)));
|
|
12075
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterIrradiance.offset, one(irradiance ? 1 : 0));
|
|
12076
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterBox.offset, one(boxChain ? 1 : 0));
|
|
12077
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterLevel.offset, one(level));
|
|
12078
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterRoughness.offset, one(roughness));
|
|
12079
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSamples.offset, one(samples));
|
|
12080
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceTexels.offset, one(this.probeSize));
|
|
12081
|
+
this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceMaxLod.offset, one(topLevel));
|
|
11641
12082
|
const pass = encoder.beginRenderPass({
|
|
11642
12083
|
label: `probe.prefilter${level}.layer${layer}`,
|
|
11643
12084
|
colorAttachments: [
|
|
11644
12085
|
{
|
|
11645
|
-
view:
|
|
11646
|
-
dimension: '2d',
|
|
11647
|
-
baseArrayLayer: layer,
|
|
11648
|
-
arrayLayerCount: 1,
|
|
11649
|
-
baseMipLevel: level,
|
|
11650
|
-
mipLevelCount: 1,
|
|
11651
|
-
}),
|
|
12086
|
+
view: targets.arrayView(prefilterTarget, layer, level),
|
|
11652
12087
|
loadOp: 'clear',
|
|
11653
12088
|
storeOp: 'store',
|
|
11654
12089
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -11656,17 +12091,7 @@ export class WebGPURenderer {
|
|
|
11656
12091
|
],
|
|
11657
12092
|
});
|
|
11658
12093
|
pass.setPipeline(prefilter);
|
|
11659
|
-
pass.setBindGroup(0,
|
|
11660
|
-
layout: this.probePrefilterLayout,
|
|
11661
|
-
entries: [
|
|
11662
|
-
{
|
|
11663
|
-
binding: PREFILTER_UNIFORM_BINDING,
|
|
11664
|
-
resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
|
|
11665
|
-
},
|
|
11666
|
-
{ binding: PREFILTER_TEXTURE_BINDING, resource: source },
|
|
11667
|
-
{ binding: PREFILTER_SAMPLER_BINDING, resource: this.probeSampler },
|
|
11668
|
-
],
|
|
11669
|
-
}), [slot]);
|
|
12094
|
+
pass.setBindGroup(0, group, [slot]);
|
|
11670
12095
|
pass.draw(3);
|
|
11671
12096
|
pass.end();
|
|
11672
12097
|
}
|
|
@@ -12130,7 +12555,7 @@ export class WebGPURenderer {
|
|
|
12130
12555
|
f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.frameHeight);
|
|
12131
12556
|
/* In scene units, which is why an HDR target is what makes a threshold above 1 mean
|
|
12132
12557
|
anything: against a clamped buffer nothing is ever brighter than white. */
|
|
12133
|
-
f[slot(0) + prefilter('uThreshold')] = this.quality.bloomThreshold;
|
|
12558
|
+
f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
|
|
12134
12559
|
/* Each downsample's texel is its *source's*, which is the level above it. */
|
|
12135
12560
|
const down = (name) => this.postField(BLOOM_DOWNSAMPLE_FIELDS, name);
|
|
12136
12561
|
for (let index = 1; index < levels.length; index++) {
|
|
@@ -12206,12 +12631,17 @@ export class WebGPURenderer {
|
|
|
12206
12631
|
drainRegistry(this.computes, (definition) => definition.dispose?.(computeDevice));
|
|
12207
12632
|
this.fieldComposer?.dispose();
|
|
12208
12633
|
this.fieldComposer = null;
|
|
12634
|
+
this.driftIndex?.destroy();
|
|
12635
|
+
this.driftAtlas?.destroy();
|
|
12209
12636
|
this.pass = null;
|
|
12210
12637
|
this.encoder = null;
|
|
12211
12638
|
this.skinPalettes.dispose();
|
|
12212
12639
|
this.gpuTimer.dispose();
|
|
12213
12640
|
this.reflection?.dispose();
|
|
12214
12641
|
this.reflection = null;
|
|
12642
|
+
this.probeTargets?.dispose();
|
|
12643
|
+
this.exposurePass?.dispose();
|
|
12644
|
+
this.exposurePass = null;
|
|
12215
12645
|
this.surface.dispose();
|
|
12216
12646
|
}
|
|
12217
12647
|
}
|