@driftengine/core 4.5.0 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +62 -0
- package/README.md +1 -1
- package/dist/geometry/boxCsg.d.ts +28 -0
- package/dist/geometry/boxCsg.js +139 -0
- package/dist/geometry/csg.d.ts +36 -0
- package/dist/geometry/csg.js +329 -0
- package/dist/geometry/meshBuilder.js +13 -0
- package/dist/geometry/meshConcat.js +1 -0
- package/dist/geometry/pixelFont.js +2 -0
- package/dist/geometry/solid.d.ts +61 -0
- package/dist/geometry/solid.js +152 -0
- package/dist/geometry/solidFlat.d.ts +35 -0
- package/dist/geometry/solidFlat.js +428 -0
- package/dist/geometry/solidHarness.d.ts +19 -0
- package/dist/geometry/solidHarness.js +66 -0
- package/dist/geometry/solidNormals.d.ts +17 -0
- package/dist/geometry/solidNormals.js +86 -0
- package/dist/geometry/solidRound.d.ts +41 -0
- package/dist/geometry/solidRound.js +323 -0
- package/dist/geometry/solidWriter.d.ts +16 -0
- package/dist/geometry/solidWriter.js +42 -0
- package/dist/index.d.ts +23 -2
- package/dist/index.js +13 -1
- package/dist/render/backend/api.d.ts +17 -3
- package/dist/render/backend/drawFog.d.ts +26 -0
- package/dist/render/backend/drawFog.js +30 -0
- package/dist/render/backend/materialChanges.d.ts +2 -1
- package/dist/render/backend/materialChanges.js +6 -1
- package/dist/render/backend/webgl2/instanced.d.ts +24 -5
- package/dist/render/backend/webgl2/instanced.js +72 -15
- package/dist/render/backend/webgl2/renderer.d.ts +122 -8
- package/dist/render/backend/webgl2/renderer.js +249 -128
- package/dist/render/backend/webgpu/buffers.d.ts +17 -0
- package/dist/render/backend/webgpu/buffers.js +34 -3
- package/dist/render/backend/webgpu/depthPass.js +5 -1
- package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
- package/dist/render/backend/webgpu/drawCommand.js +1 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
- package/dist/render/backend/webgpu/flatPass.js +38 -9
- package/dist/render/backend/webgpu/glassTintPass.js +2 -1
- package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
- package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
- package/dist/render/backend/webgpu/instanced.d.ts +11 -1
- package/dist/render/backend/webgpu/instanced.js +21 -3
- package/dist/render/backend/webgpu/latePass.d.ts +6 -2
- package/dist/render/backend/webgpu/latePass.js +11 -4
- package/dist/render/backend/webgpu/motionPass.js +2 -1
- package/dist/render/backend/webgpu/postPass.d.ts +1 -1
- package/dist/render/backend/webgpu/renderer.d.ts +100 -10
- package/dist/render/backend/webgpu/renderer.js +478 -214
- package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
- package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
- package/dist/render/driftLight/bake.d.ts +7 -0
- package/dist/render/driftLight/bake.js +61 -56
- package/dist/render/driftLight/denseField.d.ts +46 -0
- package/dist/render/driftLight/denseField.js +109 -0
- package/dist/render/driftLight/lightField.d.ts +10 -5
- package/dist/render/driftLight/lightField.js +25 -19
- package/dist/render/driftLight/presence.d.ts +50 -0
- package/dist/render/driftLight/presence.js +43 -0
- package/dist/render/driftLight/uniforms.d.ts +3 -3
- package/dist/render/driftLight/uniforms.js +5 -6
- package/dist/render/driftLight/worldLightField.d.ts +41 -0
- package/dist/render/driftLight/worldLightField.js +66 -0
- package/dist/render/fixtureAtlas.d.ts +20 -0
- package/dist/render/fixtureAtlas.js +49 -0
- package/dist/render/instanceCull.d.ts +63 -0
- package/dist/render/instanceCull.js +160 -0
- package/dist/render/instances.d.ts +15 -0
- package/dist/render/lightBudget.d.ts +22 -33
- package/dist/render/lightBudget.js +24 -35
- package/dist/render/lightGrid.d.ts +42 -0
- package/dist/render/lightGrid.js +87 -0
- package/dist/render/mesh.d.ts +23 -24
- package/dist/render/mesh.js +56 -68
- package/dist/render/occlusion.d.ts +10 -0
- package/dist/render/occlusion.js +36 -7
- package/dist/render/pointLightSelection.d.ts +7 -0
- package/dist/render/pointLightSelection.js +29 -4
- package/dist/render/rainField.d.ts +13 -0
- package/dist/render/rainField.js +27 -5
- package/dist/render/recon/lateDepth.d.ts +11 -0
- package/dist/render/recon/lateDepth.js +70 -0
- package/dist/render/recon/resolve.d.ts +34 -4
- package/dist/render/recon/resolve.js +161 -20
- package/dist/render/rendererHarness.js +4 -0
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +2 -0
- package/dist/render/shaders/depth.js +12 -9
- package/dist/render/shaders/flat/driftLight.d.ts +1 -1
- package/dist/render/shaders/flat/driftLight.js +32 -7
- package/dist/render/shaders/flat/index.js +5 -3
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +75 -23
- package/dist/render/shaders/flat/preamble.js +75 -20
- package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
- package/dist/render/shaders/flat/surfaceEffects.js +152 -0
- package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
- package/dist/render/shaders/generated/depth.wgsl.js +8 -8
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
- package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
- package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
- package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
- package/dist/render/shaders/generated/rush.wgsl.js +21 -16
- package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
- package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
- package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/text.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
- package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
- package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
- package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
- package/dist/render/shaders/rush.js +11 -1
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/sdfText.js +3 -2
- package/dist/render/shaders/text.d.ts +1 -1
- package/dist/render/shaders/text.js +18 -9
- package/dist/render/surfaceEffects.d.ts +83 -0
- package/dist/render/surfaceEffects.js +75 -0
- package/dist/render/surfaceScene.d.ts +8 -0
- package/dist/render/surfaceScene.js +8 -0
- package/dist/render/surfaceTexture.d.ts +18 -2
- package/dist/render/surfaceTexture.js +68 -15
- package/dist/render/temporalAa.d.ts +25 -6
- package/dist/render/temporalAa.js +45 -15
- package/dist/render/textureSource.d.ts +30 -0
- package/dist/render/textureSource.js +53 -0
- package/dist/render/vertexDefaults.js +2 -0
- package/dist/world/hlod.d.ts +104 -0
- package/dist/world/hlod.js +214 -0
- package/package.json +6 -6
- package/src/geometry/boxCsg.ts +186 -0
- package/src/geometry/csg.ts +384 -0
- package/src/geometry/meshBuilder.ts +15 -0
- package/src/geometry/meshConcat.ts +1 -0
- package/src/geometry/pixelFont.ts +2 -0
- package/src/geometry/solid.ts +189 -0
- package/src/geometry/solidFlat.ts +522 -0
- package/src/geometry/solidHarness.ts +83 -0
- package/src/geometry/solidNormals.ts +103 -0
- package/src/geometry/solidRound.ts +443 -0
- package/src/geometry/solidWriter.ts +58 -0
- package/src/index.ts +51 -1
- package/src/render/backend/api.ts +21 -2
- package/src/render/backend/drawFog.ts +31 -0
- package/src/render/backend/materialChanges.ts +9 -2
- package/src/render/backend/webgl2/instanced.ts +80 -14
- package/src/render/backend/webgl2/renderer.ts +311 -150
- package/src/render/backend/webgpu/buffers.ts +49 -3
- package/src/render/backend/webgpu/depthPass.ts +5 -1
- package/src/render/backend/webgpu/drawCommand.ts +6 -0
- package/src/render/backend/webgpu/flatPass.ts +41 -11
- package/src/render/backend/webgpu/glassTintPass.ts +2 -1
- package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
- package/src/render/backend/webgpu/instanced.ts +21 -3
- package/src/render/backend/webgpu/latePass.ts +12 -3
- package/src/render/backend/webgpu/motionPass.ts +2 -1
- package/src/render/backend/webgpu/renderer.ts +556 -239
- package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
- package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
- package/src/render/driftLight/bake.ts +85 -62
- package/src/render/driftLight/denseField.ts +149 -0
- package/src/render/driftLight/lightField.ts +33 -18
- package/src/render/driftLight/presence.ts +69 -0
- package/src/render/driftLight/uniforms.ts +8 -9
- package/src/render/driftLight/worldLightField.ts +83 -0
- package/src/render/fixtureAtlas.ts +63 -0
- package/src/render/instanceCull.ts +205 -0
- package/src/render/instances.ts +16 -0
- package/src/render/lightBudget.ts +25 -36
- package/src/render/lightGrid.ts +130 -0
- package/src/render/mesh.ts +64 -72
- package/src/render/occlusion.ts +51 -7
- package/src/render/pointLightSelection.ts +72 -4
- package/src/render/rainField.ts +39 -5
- package/src/render/recon/lateDepth.ts +83 -0
- package/src/render/recon/resolve.ts +206 -20
- package/src/render/rendererHarness.ts +4 -0
- package/src/render/sceneTarget.ts +4 -0
- package/src/render/shaders/depth.ts +12 -9
- package/src/render/shaders/flat/driftLight.ts +32 -7
- package/src/render/shaders/flat/index.ts +5 -3
- package/src/render/shaders/flat/main.ts +75 -23
- package/src/render/shaders/flat/preamble.ts +75 -20
- package/src/render/shaders/flat/surfaceEffects.ts +152 -0
- package/src/render/shaders/generated/depth.wgsl.ts +8 -8
- package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
- package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
- package/src/render/shaders/generated/rush.wgsl.ts +21 -16
- package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
- package/src/render/shaders/generated/text.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
- package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
- package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
- package/src/render/shaders/rush.ts +11 -1
- package/src/render/shaders/sdfText.ts +3 -2
- package/src/render/shaders/text.ts +18 -9
- package/src/render/surfaceEffects.ts +132 -0
- package/src/render/surfaceScene.ts +16 -0
- package/src/render/surfaceTexture.ts +110 -19
- package/src/render/temporalAa.ts +78 -14
- package/src/render/textureSource.ts +72 -0
- package/src/render/vertexDefaults.ts +2 -0
- package/src/world/hlod.ts +290 -0
|
@@ -2,11 +2,14 @@ import { mat4 } from 'gl-matrix';
|
|
|
2
2
|
import type { ProbeBakeOptions } from '../../reflectionProbe.ts';
|
|
3
3
|
import { probeFaceRange } from '../../reflectionProbe.ts';
|
|
4
4
|
import {
|
|
5
|
+
TEMPORAL_FLICKER,
|
|
5
6
|
TEMPORAL_HISTORY_BLEND,
|
|
6
7
|
TemporalHistory,
|
|
8
|
+
flickerRule,
|
|
7
9
|
jitterOffset,
|
|
8
10
|
jitterProjection,
|
|
9
11
|
} from '../../temporalAa.ts';
|
|
12
|
+
import type { FlickerRule } from '../../temporalAa.ts';
|
|
10
13
|
import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.ts';
|
|
11
14
|
import { TranslucentQueue } from '../../translucentQueue.ts';
|
|
12
15
|
import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.ts';
|
|
@@ -103,6 +106,8 @@ import { ProbeSweeps } from '../../probeSweeps.ts';
|
|
|
103
106
|
import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.ts';
|
|
104
107
|
import { environmentTexels } from './environmentTexels.ts';
|
|
105
108
|
import { packIesAtlas } from '../../iesProfile.ts';
|
|
109
|
+
import { fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.ts';
|
|
110
|
+
import { resolveSurfaceScene } from '../../surfaceScene.ts';
|
|
106
111
|
import { COOKIE_TILE } from '../../lightBudget.ts';
|
|
107
112
|
import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.ts';
|
|
108
113
|
import { toHalfFloats } from '../../halfFloat.ts';
|
|
@@ -485,6 +490,7 @@ import {
|
|
|
485
490
|
import { aimReflection, createGpuReflection, type GpuReflection } from './reflectionPass.ts';
|
|
486
491
|
import { FrameBudget } from '../budget.ts';
|
|
487
492
|
import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
|
|
493
|
+
import { FOG_RECEDE, fogModeOf } from '../drawFog.ts';
|
|
488
494
|
import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.ts';
|
|
489
495
|
import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.ts';
|
|
490
496
|
import { DistanceFieldScene } from '../../gi/fieldScene.ts';
|
|
@@ -540,6 +546,9 @@ import {
|
|
|
540
546
|
import { createFrustum, frustumFromViewProjection } from '../../../math/frustum.ts';
|
|
541
547
|
import type { Frustum } from '../../../math/frustum.ts';
|
|
542
548
|
import type { Bounds } from '../../../math/bounds.ts';
|
|
549
|
+
import { batchBoxVisible, cullsInstances } from '../../instanceCull.ts';
|
|
550
|
+
import { InstanceCullPass, MAX_CULL_SLOTS } from './instanceCullPass.ts';
|
|
551
|
+
import type { CullSlot } from './instanceCullPass.ts';
|
|
543
552
|
import { OcclusionBuffer } from '../../occlusion.ts';
|
|
544
553
|
import { boundsVisible } from '../../visibility.ts';
|
|
545
554
|
import { createPassRegistry, drainRegistry, passAt, registerIn, unregisterIn } from '../../pass.ts';
|
|
@@ -565,11 +574,15 @@ import type {
|
|
|
565
574
|
} from '../../pass.ts';
|
|
566
575
|
import type { Arena, FlushSchedule, ScheduledPass } from '../../frame/index.ts';
|
|
567
576
|
import { GpuInstancedBatch } from './instanced.ts';
|
|
568
|
-
import type { MeshInstances } from '../../instances.ts';
|
|
577
|
+
import type { InstancedOptions, MeshInstances } from '../../instances.ts';
|
|
569
578
|
import { cutoutOf } from '../../cutoutCaster.ts';
|
|
570
579
|
import type { CutoutCaster } from '../../cutoutCaster.ts';
|
|
571
580
|
import { LightField } from '../../driftLight/lightField.ts';
|
|
572
581
|
import type { LightFieldOptions, LightFieldSource } from '../../driftLight/lightField.ts';
|
|
582
|
+
import type { DriftLightVolumes } from '../../driftLight/presence.ts';
|
|
583
|
+
import type { DenseLightVolume } from '../../driftLight/denseField.ts';
|
|
584
|
+
import { WorldLightField } from '../../driftLight/worldLightField.ts';
|
|
585
|
+
import type { WorldLightFieldOptions } from '../../driftLight/worldLightField.ts';
|
|
573
586
|
import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.ts';
|
|
574
587
|
|
|
575
588
|
/**
|
|
@@ -1899,6 +1912,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1899
1912
|
* what the layout already declares.
|
|
1900
1913
|
*/
|
|
1901
1914
|
private bloomGroups: GPUBindGroup[] = [];
|
|
1915
|
+
/**
|
|
1916
|
+
* The picture the bloom reads, and its size: the scene target, or while reconstructing the
|
|
1917
|
+
* resolved picture at the drawing buffer's size. See `rebuildBloomGroups`.
|
|
1918
|
+
*/
|
|
1919
|
+
private bloomSourceWidth = 1;
|
|
1920
|
+
private bloomSourceHeight = 1;
|
|
1902
1921
|
private frameWidth = 1;
|
|
1903
1922
|
private frameHeight = 1;
|
|
1904
1923
|
/**
|
|
@@ -2059,6 +2078,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2059
2078
|
*/
|
|
2060
2079
|
private reconHistories: (GPUTexture | null)[] = [null, null];
|
|
2061
2080
|
private reconHistoryViews: (GPUTextureView | null)[] = [null, null];
|
|
2081
|
+
/**
|
|
2082
|
+
* Each output pixel's flicker record, a pair swapped with the histories: the temporal resolve's
|
|
2083
|
+
* anti-flicker, over the reconstruction's own period (`recon/resolve.ts`).
|
|
2084
|
+
*/
|
|
2085
|
+
private reconFlickers: (GPUTexture | null)[] = [null, null];
|
|
2086
|
+
private reconFlickerViews: (GPUTextureView | null)[] = [null, null];
|
|
2087
|
+
/** Each output pixel's held depth swing, a pair of its own: see `recon/resolve.ts`. */
|
|
2088
|
+
private reconSwings: (GPUTexture | null)[] = [null, null];
|
|
2089
|
+
private reconSwingViews: (GPUTextureView | null)[] = [null, null];
|
|
2090
|
+
/** The anti-flicker's rule for the period last asked for, kept so a frame makes none. */
|
|
2091
|
+
private reconFlickerPhases = 0;
|
|
2092
|
+
private reconFlickerRule: FlickerRule = TEMPORAL_FLICKER;
|
|
2062
2093
|
private reconWrite = 0;
|
|
2063
2094
|
private reconHasHistory = false;
|
|
2064
2095
|
private reconShown: GPUTexture | null = null;
|
|
@@ -2092,6 +2123,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2092
2123
|
/** Last frame's raw view-projection and its inverse, and the two eyes. */
|
|
2093
2124
|
private readonly reconPreviousRawViewProj = mat4.create();
|
|
2094
2125
|
private readonly reconInverseViewProj = mat4.create();
|
|
2126
|
+
/** The inverse and the reprojection, multiplied in double precision. See `runReconstruction`. */
|
|
2127
|
+
private readonly reconInverse64 = new Float64Array(16);
|
|
2128
|
+
private readonly reconReprojection64 = new Float64Array(16);
|
|
2095
2129
|
private readonly reconPreviousInverseViewProj = mat4.create();
|
|
2096
2130
|
private readonly reconEye = new Float32Array(3);
|
|
2097
2131
|
private readonly reconPreviousEye = new Float32Array(3);
|
|
@@ -2243,6 +2277,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2243
2277
|
/** Said once for the whole renderer. See `refuseOitMultisampled`. */
|
|
2244
2278
|
private oitMultisampleRefused = false;
|
|
2245
2279
|
private oitReplaying = false;
|
|
2280
|
+
/** `uSurfaceScene`, resolved once a pass. See `surfaceScene.ts`. */
|
|
2281
|
+
private readonly surfaceScene = new Float32Array(4);
|
|
2246
2282
|
/** Which buffer the replay is filling, which the pipeline key must carry. */
|
|
2247
2283
|
private oitMode: OitTarget = 'none';
|
|
2248
2284
|
/**
|
|
@@ -2534,6 +2570,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2534
2570
|
}
|
|
2535
2571
|
|
|
2536
2572
|
private flushRings(): void {
|
|
2573
|
+
/* Culls first: a submit that follows may carry a draw reading what they write. */
|
|
2574
|
+
this.instanceCulls?.flush();
|
|
2537
2575
|
for (const ring of this.rings()) ring.flush();
|
|
2538
2576
|
}
|
|
2539
2577
|
|
|
@@ -2586,7 +2624,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2586
2624
|
): void {
|
|
2587
2625
|
if (!this.canDraw()) return;
|
|
2588
2626
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
2589
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
2627
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
2590
2628
|
const vertexSlot = this.filmVerts.allocate();
|
|
2591
2629
|
const fragmentSlot = this.filmFrags.allocate();
|
|
2592
2630
|
if (vertexSlot === null || fragmentSlot === null) return;
|
|
@@ -2771,6 +2809,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2771
2809
|
return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
|
|
2772
2810
|
}
|
|
2773
2811
|
|
|
2812
|
+
/** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
|
|
2813
|
+
createSurfaceTextureArray(
|
|
2814
|
+
sources: readonly TexImageSource[],
|
|
2815
|
+
options: SurfaceTextureOptions = {},
|
|
2816
|
+
): GpuSurfaceTexture {
|
|
2817
|
+
return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options);
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2774
2820
|
/**
|
|
2775
2821
|
* GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
|
|
2776
2822
|
*
|
|
@@ -3049,6 +3095,19 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3049
3095
|
});
|
|
3050
3096
|
}
|
|
3051
3097
|
|
|
3098
|
+
/**
|
|
3099
|
+
* How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
|
|
3100
|
+
*
|
|
3101
|
+
* Written straight into the scratch block rather than through `material()`'s callback, because
|
|
3102
|
+
* this one is called per crossfading region per frame and a closure per call is an allocation.
|
|
3103
|
+
*/
|
|
3104
|
+
setDitherFade(amount: number): void {
|
|
3105
|
+
if (this.surface.lost) return;
|
|
3106
|
+
/* The second half of `uWriteMode`, which the order-independent weight shares. */
|
|
3107
|
+
this.perFrameFloats[this.materialField('uWriteMode') + 1] = Math.min(1, Math.max(-1, amount));
|
|
3108
|
+
this.materials.dirty();
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3052
3111
|
/**
|
|
3053
3112
|
* How strong the microscopic relief on the surfaces drawn next is, and how coarse.
|
|
3054
3113
|
*
|
|
@@ -4270,8 +4329,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4270
4329
|
if (!this.canDraw()) return;
|
|
4271
4330
|
/* Geometry that has not all arrived is not drawn, which is the surface's contract and what
|
|
4272
4331
|
every other mesh verb here and the other backend's volume already kept. This one drew a
|
|
4273
|
-
beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`.
|
|
4274
|
-
|
|
4332
|
+
beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`. A
|
|
4333
|
+
hull with no triangles draws nothing either: see `GpuMesh.indexCount`. */
|
|
4334
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
4275
4335
|
/* At zero it draws nothing rather than adding black, so a caller may keep the call in. That
|
|
4276
4336
|
now covers a clear night too: a beam with no medium to light is not drawn at all. */
|
|
4277
4337
|
const shown =
|
|
@@ -4972,6 +5032,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4972
5032
|
private readonly waterLayout: GPUBindGroupLayout;
|
|
4973
5033
|
/** The one-pixel stand-in, kept so the water bind group can be rebuilt without one. */
|
|
4974
5034
|
private readonly blankView: GPUTextureView;
|
|
5035
|
+
/** The blank texel as a one-layer array: the stand-in every surface-texture binding takes. */
|
|
5036
|
+
private readonly blankSurfaceView: GPUTextureView;
|
|
4975
5037
|
private readonly blankSampler: GPUSampler;
|
|
4976
5038
|
/**
|
|
4977
5039
|
* A slot per body, not one buffer rewritten before each draw.
|
|
@@ -5417,7 +5479,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5417
5479
|
this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
|
|
5418
5480
|
|
|
5419
5481
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
5420
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
5482
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5421
5483
|
const cuts = cutoutGroup !== null;
|
|
5422
5484
|
/* A two-sided caster casts from whichever face the light sees: nothing culled. */
|
|
5423
5485
|
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
@@ -5456,7 +5518,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5456
5518
|
if (pass === null) return;
|
|
5457
5519
|
const gpuBatch = batch as GpuInstancedBatch;
|
|
5458
5520
|
const geometry = gpuBatch.mesh as GpuMesh & { key?: string };
|
|
5459
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
5521
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5460
5522
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
5461
5523
|
if (count === 0) return;
|
|
5462
5524
|
const cutout = cutoutOf(material);
|
|
@@ -5530,7 +5592,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5530
5592
|
this.writeShadowWind(slot);
|
|
5531
5593
|
|
|
5532
5594
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
5533
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
5595
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5534
5596
|
pass.setPipeline(
|
|
5535
5597
|
depthPipeline(
|
|
5536
5598
|
this.pipelines,
|
|
@@ -5560,7 +5622,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5560
5622
|
const pass = this.shadowPass;
|
|
5561
5623
|
if (pass === null || data.count === 0) return;
|
|
5562
5624
|
const geometry = scatter as GpuScatter;
|
|
5563
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
5625
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5564
5626
|
|
|
5565
5627
|
const v = this.scatterDepthFloats;
|
|
5566
5628
|
const at = (name: string): number => (SCATTER_DEPTH_FIELDS[name]?.offset ?? -4) / 4;
|
|
@@ -6002,6 +6064,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6002
6064
|
* `uStaticShadowMap` rather than by anything here changing shape.
|
|
6003
6065
|
*/
|
|
6004
6066
|
const blankView = blank.createView();
|
|
6067
|
+
/* The same texel as a one-layer array, for the four material maps, which are arrays now. */
|
|
6068
|
+
const blankSurfaceView = blank.createView({ dimension: '2d-array' });
|
|
6069
|
+
this.blankSurfaceView = blankSurfaceView;
|
|
6005
6070
|
const blankSampler = device.createSampler({ label: 'flat.sampler' });
|
|
6006
6071
|
|
|
6007
6072
|
/*
|
|
@@ -6188,7 +6253,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6188
6253
|
if (name === 'uNormalMap') {
|
|
6189
6254
|
const map = this.normalMap;
|
|
6190
6255
|
return map === null
|
|
6191
|
-
? { view:
|
|
6256
|
+
? { view: blankSurfaceView, sampler: blankSampler }
|
|
6192
6257
|
: { view: map.view, sampler: map.sampler };
|
|
6193
6258
|
}
|
|
6194
6259
|
/* The caller's ORM map, or the stand-in, for the reason `uNormalMap` gives just above. */
|
|
@@ -6196,21 +6261,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6196
6261
|
if (name === 'uEmissiveMap') {
|
|
6197
6262
|
const map = this.emissiveMap;
|
|
6198
6263
|
return map === null
|
|
6199
|
-
? { view:
|
|
6264
|
+
? { view: blankSurfaceView, sampler: blankSampler }
|
|
6200
6265
|
: { view: map.view, sampler: map.sampler };
|
|
6201
6266
|
}
|
|
6202
6267
|
if (name === 'uOrmMap') {
|
|
6203
6268
|
const map = this.ormMap;
|
|
6204
6269
|
return map === null
|
|
6205
|
-
? { view:
|
|
6270
|
+
? { view: blankSurfaceView, sampler: blankSampler }
|
|
6206
6271
|
: { view: map.view, sampler: map.sampler };
|
|
6207
6272
|
}
|
|
6208
6273
|
if (name === 'uAlbedo') {
|
|
6209
6274
|
const albedo = this.albedo;
|
|
6210
6275
|
return albedo === null
|
|
6211
|
-
? { view:
|
|
6276
|
+
? { view: blankSurfaceView, sampler: blankSampler }
|
|
6212
6277
|
: { view: albedo.view, sampler: albedo.sampler };
|
|
6213
6278
|
}
|
|
6279
|
+
/* The albedo's effects table, or a one-texel stand-in the shader's size test reads as none.
|
|
6280
|
+
The albedo is the group's key, so its table needs no key of its own. */
|
|
6281
|
+
if (name === 'uSurfaceEffects') {
|
|
6282
|
+
return { view: this.albedo?.effectsView ?? blankView, sampler: blankSampler };
|
|
6283
|
+
}
|
|
6214
6284
|
/*
|
|
6215
6285
|
* The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
|
|
6216
6286
|
*
|
|
@@ -6242,18 +6312,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6242
6312
|
* this one is a curve sampled between two angles rather than data addressed by index, so
|
|
6243
6313
|
* interpolation across a row is the point. `postSampler` is linear and clamped.
|
|
6244
6314
|
*/
|
|
6245
|
-
if (name === '
|
|
6246
|
-
return { view: this.
|
|
6247
|
-
}
|
|
6248
|
-
/*
|
|
6249
|
-
* The cookie atlas, or the single white texel a scene with no cookie falls back to.
|
|
6250
|
-
*
|
|
6251
|
-
* A filtering sampler, like the photometric atlas and unlike the froxel table: a cookie is
|
|
6252
|
-
* an image and its whole point is that it is soft at the edges. The shader's own half-texel
|
|
6253
|
-
* inset is what keeps one tile out of the next; a clamp cannot, the seam being interior.
|
|
6254
|
-
*/
|
|
6255
|
-
if (name === 'uCookieAtlas') {
|
|
6256
|
-
return { view: this.cookieView ?? blankView, sampler: iesSampler };
|
|
6315
|
+
if (name === 'uFixtureAtlas') {
|
|
6316
|
+
return { view: this.fixtureAtlasView(), sampler: iesSampler };
|
|
6257
6317
|
}
|
|
6258
6318
|
/*
|
|
6259
6319
|
* DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
|
|
@@ -7106,6 +7166,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7106
7166
|
if (!attribute.optional) continue;
|
|
7107
7167
|
present[String(attribute.name)] = key.includes(`:${String(attribute.name)}`);
|
|
7108
7168
|
}
|
|
7169
|
+
/* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
|
|
7170
|
+
present['layers'] = key.includes(':layers');
|
|
7109
7171
|
return present;
|
|
7110
7172
|
}
|
|
7111
7173
|
|
|
@@ -7118,6 +7180,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7118
7180
|
* uncaught exceptions a second in somebody's error reporting.
|
|
7119
7181
|
*/
|
|
7120
7182
|
beginFrame(clearColor: Vec3): void {
|
|
7183
|
+
this.cullFrame += 1;
|
|
7121
7184
|
/* Whatever an update replaced during the last frame; nothing recorded can read it now. */
|
|
7122
7185
|
for (const texture of this.retiredTextures) texture.destroy();
|
|
7123
7186
|
this.retiredTextures.length = 0;
|
|
@@ -7380,6 +7443,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7380
7443
|
present[String(attribute.name)] = supplied;
|
|
7381
7444
|
key += supplied ? `:${String(attribute.name)}` : '';
|
|
7382
7445
|
}
|
|
7446
|
+
/*
|
|
7447
|
+
* Texture-array layers widen the UV field to three floats (`LAYERED_UVS`), which is a different
|
|
7448
|
+
* vertex layout, so the key has to say so or a layered and a plain mesh would share a pipeline
|
|
7449
|
+
* and one of them would read every field after the UVs from the wrong offset.
|
|
7450
|
+
*/
|
|
7451
|
+
if (data.layers !== undefined) {
|
|
7452
|
+
present['layers'] = true;
|
|
7453
|
+
key += ':layers';
|
|
7454
|
+
}
|
|
7383
7455
|
const fullKey = `${this.variant}|${key}`;
|
|
7384
7456
|
/*
|
|
7385
7457
|
* **For every target this mesh can land on, which is two when the overlay is its own.**
|
|
@@ -7605,62 +7677,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7605
7677
|
* Its sequence is its own: `reconJitterPhases` takes eight positions for every output pixel a
|
|
7606
7678
|
* render pixel covers, where the temporal resolve's eight are enough at one to one.
|
|
7607
7679
|
*/
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
*
|
|
7618
|
-
* **The y offset is negated on the way in, and that is not a preference.** This shifts the
|
|
7619
|
-
* *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
|
|
7620
|
-
* every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
|
|
7621
|
-
* arrives on screen with its sign reversed, while x, which nothing negates, arrives as
|
|
7622
|
-
* given. The temporal resolve cannot see this because its shader never reads the offset; the
|
|
7623
|
-
* reconstruction reads it at every texel it unprojects, and a sign error there moves every
|
|
7624
|
-
* sample two jitters away from where the depth says it is.
|
|
7625
|
-
*/
|
|
7626
|
-
this.viewProj = jitterProjection(
|
|
7627
|
-
this.jitteredViewProj,
|
|
7628
|
-
this.correctedViewProj,
|
|
7629
|
-
this.reconJitter[0] as number,
|
|
7630
|
-
-(this.reconJitter[1] as number),
|
|
7631
|
-
this.renderWidth,
|
|
7632
|
-
this.renderHeight,
|
|
7633
|
-
) as Float32Array;
|
|
7634
|
-
this.temporalJittering = false;
|
|
7635
|
-
this.temporalHistoryUsable = false;
|
|
7636
|
-
}
|
|
7637
|
-
this.temporalJittering =
|
|
7638
|
-
!this.reconstructing &&
|
|
7639
|
-
this.quality.temporalAa &&
|
|
7640
|
-
this.quality.screenEffects &&
|
|
7641
|
-
!this.reflectionPassActive;
|
|
7642
|
-
if (this.temporalJittering) {
|
|
7643
|
-
/* The scene's size, not the drawing buffer's: the history is a picture of the render. */
|
|
7644
|
-
this.temporalHistoryUsable = this.temporalHistory.openFrame(
|
|
7645
|
-
this.renderWidth,
|
|
7646
|
-
this.renderHeight,
|
|
7647
|
-
);
|
|
7648
|
-
const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
|
|
7649
|
-
this.temporalJitterX = jx;
|
|
7650
|
-
this.temporalJitterY = jy;
|
|
7651
|
-
/* A jitter is half a *render* texel, which is what the sequence is spread over. */
|
|
7652
|
-
this.viewProj = jitterProjection(
|
|
7653
|
-
this.jitteredViewProj,
|
|
7654
|
-
this.correctedViewProj,
|
|
7655
|
-
jx,
|
|
7656
|
-
jy,
|
|
7657
|
-
this.renderWidth,
|
|
7658
|
-
this.renderHeight,
|
|
7659
|
-
) as Float32Array;
|
|
7660
|
-
} else if (!this.reconstructing) {
|
|
7661
|
-
this.temporalHistoryUsable = false;
|
|
7662
|
-
this.viewProj = this.correctedViewProj;
|
|
7663
|
-
}
|
|
7680
|
+
/*
|
|
7681
|
+
* **After the present there is no jitter to add and no temporal frame to open.** A mesh pass
|
|
7682
|
+
* bound then is the interface's, drawn over a picture already resolved: jittered, a map and every
|
|
7683
|
+
* edge of it moved by a fraction of a pixel each frame with nothing to resolve it; and opening a
|
|
7684
|
+
* second temporal frame stepped the sequence twice a frame, so the world saw every other
|
|
7685
|
+
* position and the anti-flicker's period never started.
|
|
7686
|
+
*/
|
|
7687
|
+
if (this.framePresented) this.viewProj = this.correctedViewProj;
|
|
7688
|
+
else this.jitterFrame();
|
|
7664
7689
|
/*
|
|
7665
7690
|
* The frustum, once a frame, and from the **uncorrected** matrix.
|
|
7666
7691
|
*
|
|
@@ -7760,8 +7785,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7760
7785
|
* frame either way, because an unwritten one is four zeroes and `dot(p, 0) < 0` is false,
|
|
7761
7786
|
* so the term would silently pass everything the first time somebody enabled it.
|
|
7762
7787
|
*/
|
|
7763
|
-
|
|
7764
|
-
|
|
7788
|
+
/* The mirror's plane in a reflection pass and zero, which clips nothing, in every other: the lit
|
|
7789
|
+
stage has no flag beside the plane. See `uClipPlane` in `flat/preamble.ts`. */
|
|
7790
|
+
f.set(
|
|
7791
|
+
this.reflectionPassActive ? (this.reflection?.clipPlane ?? NO_CLIP_PLANE) : NO_CLIP_PLANE,
|
|
7792
|
+
at('uClipPlane'),
|
|
7793
|
+
);
|
|
7794
|
+
resolveSurfaceScene(env, this.surfaceScene);
|
|
7795
|
+
f.set(this.surfaceScene, at('uSurfaceScene'));
|
|
7765
7796
|
|
|
7766
7797
|
/*
|
|
7767
7798
|
* The material and grading terms, matching `renderer.ts` value for value.
|
|
@@ -7782,6 +7813,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7782
7813
|
*/
|
|
7783
7814
|
f[at('uGrain')] = 1;
|
|
7784
7815
|
f[at('uReflectivity')] = 0;
|
|
7816
|
+
/* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
|
|
7817
|
+
f[at('uWriteMode')] = 0;
|
|
7818
|
+
f[at('uWriteMode') + 1] = 0;
|
|
7785
7819
|
/* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
|
|
7786
7820
|
f[at('uEnvironmentGain')] = 1;
|
|
7787
7821
|
/*
|
|
@@ -8341,7 +8375,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8341
8375
|
* **Recorded rather than drawn**, when the effect is on and this is not already the replay.
|
|
8342
8376
|
* Everything is copied — see `TranslucentQueue`, and the scratch matrix that made it necessary.
|
|
8343
8377
|
*/
|
|
8344
|
-
if (this.oitActive && !this.oitReplaying) {
|
|
8378
|
+
if (this.oitActive && !this.oitReplaying && options.additive !== true) {
|
|
8345
8379
|
this.translucentQueue.record(mesh, model, opacity, options);
|
|
8346
8380
|
return;
|
|
8347
8381
|
}
|
|
@@ -8389,8 +8423,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8389
8423
|
options: TranslucentMeshOptions = {},
|
|
8390
8424
|
): void {
|
|
8391
8425
|
if (!this.canDraw() || this.viewProj === null) return;
|
|
8392
|
-
/* Geometry that has not all arrived is not drawn. See `GpuMesh.complete
|
|
8393
|
-
|
|
8426
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `GpuMesh.complete`
|
|
8427
|
+
and `GpuMesh.indexCount`. */
|
|
8428
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
8394
8429
|
|
|
8395
8430
|
const slot = this.perDraw.allocate();
|
|
8396
8431
|
if (slot === null) {
|
|
@@ -8404,7 +8439,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8404
8439
|
}
|
|
8405
8440
|
|
|
8406
8441
|
/* A blended draw landing after the upscale is drawn into an unjittered picture. */
|
|
8407
|
-
|
|
8442
|
+
/* Unless it moves with what it lies on, which the resolve reconstructs it with. See
|
|
8443
|
+
`TranslucentMeshOptions.reconstructed`. */
|
|
8444
|
+
const lands = blend && options.reconstructed !== true && this.drawsLate();
|
|
8445
|
+
const late = lands || (blend && this.oitReplaying && this.reconstructing);
|
|
8408
8446
|
this.perDraw.writeFloats(
|
|
8409
8447
|
slot,
|
|
8410
8448
|
FLAT_VERT_FIELDS.uViewProj.offset,
|
|
@@ -8443,17 +8481,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8443
8481
|
this.perFrameFloats[this.materialField('uOpacity')] = opacity;
|
|
8444
8482
|
/* Material state exactly like `uOpacity`, dirtied for the accumulation pass and restored
|
|
8445
8483
|
below — the revealage pass draws with it at zero and its colour multiplied away. */
|
|
8446
|
-
this.perFrameFloats[this.materialField('
|
|
8484
|
+
this.perFrameFloats[this.materialField('uWriteMode')] = this.oitMode === 'accum' ? 1 : 0;
|
|
8447
8485
|
}
|
|
8448
8486
|
/* Same shape as `dimmed`: a slot is only worth taking when either differs from the default
|
|
8449
8487
|
`bindMeshPass` already wrote, and both are put back together once the draw is submitted. */
|
|
8450
8488
|
const lit = options.lit ?? true;
|
|
8451
|
-
|
|
8489
|
+
/* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
|
|
8490
|
+
const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
|
|
8452
8491
|
const toneMapped = options.toneMapped ?? true;
|
|
8453
|
-
const unlitOrUnfogged = !lit ||
|
|
8492
|
+
const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
|
|
8454
8493
|
if (unlitOrUnfogged) {
|
|
8455
8494
|
if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
|
|
8456
|
-
|
|
8495
|
+
this.perFrameInts[this.materialField('uFogEnabled')] = fog;
|
|
8457
8496
|
}
|
|
8458
8497
|
/* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
|
|
8459
8498
|
a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
|
|
@@ -8498,8 +8537,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8498
8537
|
*/
|
|
8499
8538
|
const depthWrite = options.depthWrite ?? true;
|
|
8500
8539
|
const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
|
|
8540
|
+
/* Added rather than blended over: its own blend state, so its own pipeline. */
|
|
8541
|
+
const adds = blend && options.additive === true;
|
|
8501
8542
|
const key =
|
|
8502
|
-
`${keyed}${blend ? '|blend' : ''}` +
|
|
8543
|
+
`${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
|
|
8503
8544
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
8504
8545
|
/* The two order-independent buffers are two formats and two blend states, so a draw into
|
|
8505
8546
|
one must never share a cached pipeline with the same mesh drawn into the other. */
|
|
@@ -8529,7 +8570,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8529
8570
|
* pays the compile it was trying to avoid. That is a slow frame; the alternative is a
|
|
8530
8571
|
* thrown error in the middle of one.
|
|
8531
8572
|
*/
|
|
8532
|
-
const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
|
|
8573
|
+
const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
|
|
8533
8574
|
let pipeline = pipelines.peek(key);
|
|
8534
8575
|
if (pipeline === undefined) {
|
|
8535
8576
|
const present = this.meshPresent.get(base);
|
|
@@ -8543,7 +8584,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8543
8584
|
this.variant,
|
|
8544
8585
|
key,
|
|
8545
8586
|
present,
|
|
8546
|
-
blend,
|
|
8587
|
+
adds ? 'additive' : blend,
|
|
8547
8588
|
skinned,
|
|
8548
8589
|
morphed,
|
|
8549
8590
|
depthWrite,
|
|
@@ -8577,7 +8618,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8577
8618
|
* the uniform rings are the state arena this design would otherwise have had to build —
|
|
8578
8619
|
* so the command carries them as its two dynamic offsets.
|
|
8579
8620
|
*/
|
|
8580
|
-
const command = blend
|
|
8621
|
+
const command = blend
|
|
8622
|
+
? this.recordBlended(0, lands)
|
|
8623
|
+
: this.recordDraw(0, this.currentTarget());
|
|
8581
8624
|
if (command !== null) {
|
|
8582
8625
|
command.pipeline = pipeline;
|
|
8583
8626
|
command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
|
|
@@ -8608,11 +8651,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8608
8651
|
whether or not this one found a slot to draw with. */
|
|
8609
8652
|
if (dimmed) {
|
|
8610
8653
|
this.perFrameFloats[this.materialField('uOpacity')] = 1;
|
|
8611
|
-
this.perFrameFloats[this.materialField('
|
|
8654
|
+
this.perFrameFloats[this.materialField('uWriteMode')] = 0;
|
|
8612
8655
|
}
|
|
8613
8656
|
if (unlitOrUnfogged) {
|
|
8614
8657
|
if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
|
|
8615
|
-
|
|
8658
|
+
this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
|
|
8616
8659
|
}
|
|
8617
8660
|
/* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
|
|
8618
8661
|
shared, so a strength left set is worn by everything drawn after it. */
|
|
@@ -8638,7 +8681,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8638
8681
|
* anyway, because WebGL2 *cannot* and a capability one backend has and the other does not is
|
|
8639
8682
|
* the shape this repository spent a week removing.
|
|
8640
8683
|
*/
|
|
8641
|
-
createInstanced(
|
|
8684
|
+
createInstanced(
|
|
8685
|
+
mesh: GpuMesh,
|
|
8686
|
+
capacity: number,
|
|
8687
|
+
options: InstancedOptions = {},
|
|
8688
|
+
): GpuInstancedBatch {
|
|
8642
8689
|
/*
|
|
8643
8690
|
* **A skinned mesh cannot be instanced, and the reason is the vertex layout rather than the
|
|
8644
8691
|
* shader.** An instanced pipeline reclaims locations 11 and 12 from the joint indices and
|
|
@@ -8668,24 +8715,20 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8668
8715
|
'instanced pipeline.',
|
|
8669
8716
|
);
|
|
8670
8717
|
}
|
|
8671
|
-
if (this.instanced.has(mesh)) {
|
|
8672
|
-
throw new Error(
|
|
8673
|
-
'WebGPU: this mesh already has an instanced batch — a mesh may have one, because the ' +
|
|
8674
|
-
"other backend binds the attributes to the mesh's own vertex array.",
|
|
8675
|
-
);
|
|
8676
|
-
}
|
|
8677
8718
|
const batch = new GpuInstancedBatch(
|
|
8678
8719
|
this.surface.device,
|
|
8679
8720
|
mesh,
|
|
8680
8721
|
capacity,
|
|
8681
8722
|
`flat.instances:${(mesh as GpuMesh & { key?: string }).key ?? 'mesh'}`,
|
|
8723
|
+
options.cull ?? false,
|
|
8682
8724
|
);
|
|
8683
|
-
this.instanced.set(mesh, batch);
|
|
8684
8725
|
return batch;
|
|
8685
8726
|
}
|
|
8686
8727
|
|
|
8687
|
-
/**
|
|
8688
|
-
private
|
|
8728
|
+
/** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
|
|
8729
|
+
private instanceCulls: InstanceCullPass | null = null;
|
|
8730
|
+
/** Counts frames, so a batch knows when its cull slots were last taken. */
|
|
8731
|
+
private cullFrame = 0;
|
|
8689
8732
|
|
|
8690
8733
|
/** Push placement and colour. Only the live prefix; see `GpuInstancedBatch.upload`. */
|
|
8691
8734
|
uploadInstanced(batch: GpuInstancedBatch, data: MeshInstances): void {
|
|
@@ -8733,7 +8776,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8733
8776
|
|
|
8734
8777
|
/** Release the placement. The mesh is the caller's and is not released. */
|
|
8735
8778
|
disposeInstanced(batch: GpuInstancedBatch): void {
|
|
8736
|
-
this.instanced.delete(batch.mesh);
|
|
8737
8779
|
batch.dispose();
|
|
8738
8780
|
}
|
|
8739
8781
|
|
|
@@ -8788,6 +8830,37 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8788
8830
|
* **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
|
|
8789
8831
|
* is built, and a per-instance opacity: `opacity` here dims the whole batch.
|
|
8790
8832
|
*/
|
|
8833
|
+
/**
|
|
8834
|
+
* The next of a batch's cull slots this frame, with its cull queued against the current view; null
|
|
8835
|
+
* when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
|
|
8836
|
+
* and the draw then takes every instance.
|
|
8837
|
+
*/
|
|
8838
|
+
private takeCullSlot(batch: GpuInstancedBatch, count: number): CullSlot | null {
|
|
8839
|
+
if (batch.slotFrame !== this.cullFrame) {
|
|
8840
|
+
batch.slotFrame = this.cullFrame;
|
|
8841
|
+
batch.slotsTaken = 0;
|
|
8842
|
+
}
|
|
8843
|
+
if (batch.slotsTaken >= MAX_CULL_SLOTS) return null;
|
|
8844
|
+
this.instanceCulls ??= new InstanceCullPass(this.surface.device);
|
|
8845
|
+
let slot = batch.slots[batch.slotsTaken];
|
|
8846
|
+
if (slot === undefined) {
|
|
8847
|
+
slot = this.instanceCulls.createSlot(batch.buffer, batch.capacity, 'flat.instances');
|
|
8848
|
+
batch.slots.push(slot);
|
|
8849
|
+
}
|
|
8850
|
+
const bounds = batch.mesh.bounds;
|
|
8851
|
+
const queued = this.instanceCulls.queue(
|
|
8852
|
+
slot,
|
|
8853
|
+
this.frustum,
|
|
8854
|
+
bounds.centre,
|
|
8855
|
+
bounds.radius,
|
|
8856
|
+
count,
|
|
8857
|
+
batch.mesh.indexCount,
|
|
8858
|
+
);
|
|
8859
|
+
if (!queued) return null;
|
|
8860
|
+
batch.slotsTaken += 1;
|
|
8861
|
+
return slot;
|
|
8862
|
+
}
|
|
8863
|
+
|
|
8791
8864
|
private submitInstanced(
|
|
8792
8865
|
batch: GpuInstancedBatch,
|
|
8793
8866
|
data: MeshInstances,
|
|
@@ -8797,9 +8870,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8797
8870
|
): void {
|
|
8798
8871
|
if (!this.canDraw() || this.viewProj === null) return;
|
|
8799
8872
|
const mesh = batch.mesh;
|
|
8800
|
-
|
|
8873
|
+
/* Nor a batch of a mesh with nothing in it: see `GpuMesh.indexCount`. */
|
|
8874
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
8801
8875
|
const count = Math.min(data.count, batch.capacity);
|
|
8802
8876
|
if (count === 0) return;
|
|
8877
|
+
/* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
|
|
8878
|
+
then instance by instance on the device into an indirect draw. Blended draws keep every
|
|
8879
|
+
instance and their order. See `InstancedOptions.cull`. */
|
|
8880
|
+
const culling = !blend && batch.cull;
|
|
8881
|
+
if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
|
|
8882
|
+
const cut =
|
|
8883
|
+
culling && cullsInstances(count, mesh.indexCount) ? this.takeCullSlot(batch, count) : null;
|
|
8884
|
+
const instanceBuffer = cut === null ? batch.buffer : cut.instances;
|
|
8803
8885
|
|
|
8804
8886
|
const slot = this.perDraw.allocate();
|
|
8805
8887
|
if (slot === null) {
|
|
@@ -8812,10 +8894,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8812
8894
|
return;
|
|
8813
8895
|
}
|
|
8814
8896
|
|
|
8897
|
+
/* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
|
|
8898
|
+
const lands = blend && options.reconstructed !== true && this.drawsLate();
|
|
8815
8899
|
this.perDraw.writeFloats(
|
|
8816
8900
|
slot,
|
|
8817
8901
|
FLAT_VERT_FIELDS.uViewProj.offset,
|
|
8818
|
-
|
|
8902
|
+
lands ? this.correctedViewProj : this.viewProj,
|
|
8819
8903
|
);
|
|
8820
8904
|
this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
|
|
8821
8905
|
this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
|
|
@@ -8826,12 +8910,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8826
8910
|
this.perFrameFloats[this.materialField('uOpacity')] = opacity;
|
|
8827
8911
|
}
|
|
8828
8912
|
const lit = options.lit ?? true;
|
|
8829
|
-
const fog = options.fog ?? true;
|
|
8913
|
+
const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
|
|
8830
8914
|
const toneMapped = options.toneMapped ?? true;
|
|
8831
|
-
const unlitOrUnfogged = !lit ||
|
|
8915
|
+
const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
|
|
8832
8916
|
if (unlitOrUnfogged) {
|
|
8833
8917
|
if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
|
|
8834
|
-
|
|
8918
|
+
this.perFrameInts[this.materialField('uFogEnabled')] = fog;
|
|
8835
8919
|
}
|
|
8836
8920
|
if (!toneMapped) {
|
|
8837
8921
|
this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
|
|
@@ -8847,12 +8931,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8847
8931
|
/* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
|
|
8848
8932
|
a third vertex buffer and a different vertex module, and sharing a cache entry with the
|
|
8849
8933
|
plain one would hand a draw the wrong layout. */
|
|
8934
|
+
const adds = blend && options.additive === true;
|
|
8850
8935
|
const key =
|
|
8851
|
-
`${base}|inst${blend ? '|blend' : ''}` +
|
|
8936
|
+
`${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
|
|
8852
8937
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
8853
8938
|
`${this.materialDoubleSided ? '|2s' : ''}`;
|
|
8854
8939
|
|
|
8855
|
-
const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
|
|
8940
|
+
const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
|
|
8856
8941
|
let pipeline = pipelines.peek(key);
|
|
8857
8942
|
if (pipeline === undefined) {
|
|
8858
8943
|
const present = this.meshPresent.get(base);
|
|
@@ -8866,7 +8951,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8866
8951
|
this.variant,
|
|
8867
8952
|
key,
|
|
8868
8953
|
present,
|
|
8869
|
-
blend,
|
|
8954
|
+
adds ? 'additive' : blend,
|
|
8870
8955
|
false,
|
|
8871
8956
|
false,
|
|
8872
8957
|
depthWrite,
|
|
@@ -8879,7 +8964,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8879
8964
|
|
|
8880
8965
|
const material = this.materialSlotForDraw();
|
|
8881
8966
|
if (material !== null) {
|
|
8882
|
-
const command = blend
|
|
8967
|
+
const command = blend
|
|
8968
|
+
? this.recordBlended(0, lands)
|
|
8969
|
+
: this.recordDraw(0, this.currentTarget());
|
|
8883
8970
|
if (command !== null) {
|
|
8884
8971
|
command.pipeline = pipeline;
|
|
8885
8972
|
command.bindGroup = this.flatBindGroupFor(false, false, null);
|
|
@@ -8890,12 +8977,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8890
8977
|
for (; index < mesh.vertexBuffers.length; index++) {
|
|
8891
8978
|
command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
|
|
8892
8979
|
}
|
|
8893
|
-
command.vertexBuffers[index] =
|
|
8980
|
+
command.vertexBuffers[index] = instanceBuffer;
|
|
8894
8981
|
command.vertexCount = mesh.vertexBuffers.length + 1;
|
|
8895
8982
|
command.indexBuffer = mesh.indexBuffer;
|
|
8896
8983
|
command.indexed = true;
|
|
8897
8984
|
command.count = mesh.indexCount;
|
|
8898
8985
|
command.instances = count;
|
|
8986
|
+
command.indirect = cut === null ? null : cut.args;
|
|
8899
8987
|
} else {
|
|
8900
8988
|
const pass = this.openPass();
|
|
8901
8989
|
if (pass === null) return;
|
|
@@ -8905,9 +8993,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8905
8993
|
for (; index < mesh.vertexBuffers.length; index++) {
|
|
8906
8994
|
pass.setVertexBuffer(index, mesh.vertexBuffers[index] as GPUBuffer);
|
|
8907
8995
|
}
|
|
8908
|
-
pass.setVertexBuffer(index,
|
|
8996
|
+
pass.setVertexBuffer(index, instanceBuffer);
|
|
8909
8997
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
8910
|
-
pass.drawIndexed(mesh.indexCount, count);
|
|
8998
|
+
if (cut === null) pass.drawIndexed(mesh.indexCount, count);
|
|
8999
|
+
else pass.drawIndexedIndirect(cut.args, 0);
|
|
8911
9000
|
}
|
|
8912
9001
|
}
|
|
8913
9002
|
/* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
|
|
@@ -8918,7 +9007,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8918
9007
|
}
|
|
8919
9008
|
if (unlitOrUnfogged) {
|
|
8920
9009
|
this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
|
|
8921
|
-
this.perFrameInts[this.materialField('uFogEnabled')] =
|
|
9010
|
+
this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
|
|
8922
9011
|
}
|
|
8923
9012
|
if (!toneMapped) {
|
|
8924
9013
|
this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
|
|
@@ -9436,7 +9525,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9436
9525
|
const tint = this.glassTint;
|
|
9437
9526
|
if (pass === null || tint === null) return;
|
|
9438
9527
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
9439
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
9528
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
9440
9529
|
const cutout = cutoutOf(material);
|
|
9441
9530
|
const albedo = cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
|
|
9442
9531
|
const slot = this.shadowDraws.allocate();
|
|
@@ -9479,7 +9568,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9479
9568
|
if (pass === null || tint === null) return;
|
|
9480
9569
|
const gpuBatch = batch as GpuInstancedBatch;
|
|
9481
9570
|
const geometry = gpuBatch.mesh as GpuMesh & { key?: string };
|
|
9482
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
9571
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
9483
9572
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
9484
9573
|
if (count === 0) return;
|
|
9485
9574
|
const cutout = cutoutOf(material);
|
|
@@ -9523,7 +9612,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9523
9612
|
const tint = this.glassTint;
|
|
9524
9613
|
if (pass === null || tint === null) return;
|
|
9525
9614
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
9526
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
9615
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
9527
9616
|
const paletteSlot = this.skinPalettes.take(this.surface.device, palette);
|
|
9528
9617
|
const view = paletteSlot === null ? null : this.skinPalettes.view(paletteSlot);
|
|
9529
9618
|
if (view === null) return;
|
|
@@ -10456,6 +10545,23 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10456
10545
|
f[at('uMotionStrength')] = motionStrength;
|
|
10457
10546
|
f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
|
|
10458
10547
|
f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
|
|
10548
|
+
/*
|
|
10549
|
+
* The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
|
|
10550
|
+
* depth and applied to a picture the jitter has been taken out of. A texel stands at its index
|
|
10551
|
+
* plus a half less the jitter, so the occlusion of a point is a jitter further on. The
|
|
10552
|
+
* reconstruction's y is given upward and negated into the corrected matrix, which this pass's
|
|
10553
|
+
* rows run down from; the temporal resolve's goes in as it is, and arrives as it is.
|
|
10554
|
+
*/
|
|
10555
|
+
f[at('uAoOffset')] = this.reconstructing
|
|
10556
|
+
? (this.reconJitter[0] as number) / this.renderWidth
|
|
10557
|
+
: this.temporalJittering
|
|
10558
|
+
? this.temporalJitterX / this.renderWidth
|
|
10559
|
+
: 0;
|
|
10560
|
+
f[at('uAoOffset') + 1] = this.reconstructing
|
|
10561
|
+
? -(this.reconJitter[1] as number) / this.renderHeight
|
|
10562
|
+
: this.temporalJittering
|
|
10563
|
+
? this.temporalJitterY / this.renderHeight
|
|
10564
|
+
: 0;
|
|
10459
10565
|
f[at('uDofStrength')] = dofStrength;
|
|
10460
10566
|
if (dofStrength > 0 && this.frameProjection !== null) {
|
|
10461
10567
|
mat4.invert(this.dofInvProjection, this.frameProjection);
|
|
@@ -10615,6 +10721,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10615
10721
|
|
|
10616
10722
|
for (const dead of [
|
|
10617
10723
|
...this.reconHistories,
|
|
10724
|
+
...this.reconFlickers,
|
|
10725
|
+
...this.reconSwings,
|
|
10618
10726
|
this.reconShown,
|
|
10619
10727
|
this.reconMotion,
|
|
10620
10728
|
this.reconPreviousDepth,
|
|
@@ -10623,6 +10731,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10623
10731
|
}
|
|
10624
10732
|
this.reconHistories = [null, null];
|
|
10625
10733
|
this.reconHistoryViews = [null, null];
|
|
10734
|
+
this.reconFlickers = [null, null];
|
|
10735
|
+
this.reconFlickerViews = [null, null];
|
|
10736
|
+
this.reconSwings = [null, null];
|
|
10737
|
+
this.reconSwingViews = [null, null];
|
|
10626
10738
|
this.reconShown = null;
|
|
10627
10739
|
this.reconShownView = null;
|
|
10628
10740
|
this.reconMotion = null;
|
|
@@ -10647,6 +10759,22 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10647
10759
|
});
|
|
10648
10760
|
this.reconHistories[i] = texture;
|
|
10649
10761
|
this.reconHistoryViews[i] = texture.createView();
|
|
10762
|
+
const record = device.createTexture({
|
|
10763
|
+
label: `recon.flicker${String(i)}`,
|
|
10764
|
+
size: [outputWidth, outputHeight],
|
|
10765
|
+
format: RECON_HISTORY_FORMAT,
|
|
10766
|
+
usage: HISTORY_USAGE,
|
|
10767
|
+
});
|
|
10768
|
+
this.reconFlickers[i] = record;
|
|
10769
|
+
this.reconFlickerViews[i] = record.createView();
|
|
10770
|
+
const swing = device.createTexture({
|
|
10771
|
+
label: `recon.swing${String(i)}`,
|
|
10772
|
+
size: [outputWidth, outputHeight],
|
|
10773
|
+
format: 'r32float',
|
|
10774
|
+
usage: HISTORY_USAGE,
|
|
10775
|
+
});
|
|
10776
|
+
this.reconSwings[i] = swing;
|
|
10777
|
+
this.reconSwingViews[i] = swing.createView();
|
|
10650
10778
|
}
|
|
10651
10779
|
this.reconShown = device.createTexture({
|
|
10652
10780
|
label: 'recon.shown',
|
|
@@ -10723,6 +10851,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10723
10851
|
viewDimension: '2d' as const,
|
|
10724
10852
|
},
|
|
10725
10853
|
},
|
|
10854
|
+
sampled(8, 'float'),
|
|
10855
|
+
{
|
|
10856
|
+
binding: 9,
|
|
10857
|
+
visibility: COMPUTE,
|
|
10858
|
+
storageTexture: {
|
|
10859
|
+
access: 'write-only' as const,
|
|
10860
|
+
format: RECON_HISTORY_FORMAT,
|
|
10861
|
+
viewDimension: '2d' as const,
|
|
10862
|
+
},
|
|
10863
|
+
},
|
|
10864
|
+
sampled(10, 'unfilterable-float'),
|
|
10865
|
+
{
|
|
10866
|
+
binding: 11,
|
|
10867
|
+
visibility: COMPUTE,
|
|
10868
|
+
storageTexture: {
|
|
10869
|
+
access: 'write-only' as const,
|
|
10870
|
+
format: 'r32float' as const,
|
|
10871
|
+
viewDimension: '2d' as const,
|
|
10872
|
+
},
|
|
10873
|
+
},
|
|
10726
10874
|
],
|
|
10727
10875
|
});
|
|
10728
10876
|
const module = device.createShaderModule({ label: 'recon.resolve', code: reconResolveWgsl() });
|
|
@@ -10754,7 +10902,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10754
10902
|
if (this.reconMotionView === null || this.reconPreviousDepthView === null) return;
|
|
10755
10903
|
if (this.reconShownView === null) return;
|
|
10756
10904
|
const { device } = this.surface;
|
|
10757
|
-
const build = (
|
|
10905
|
+
const build = (
|
|
10906
|
+
label: string,
|
|
10907
|
+
history: GPUTextureView,
|
|
10908
|
+
target: GPUTextureView,
|
|
10909
|
+
records: GPUTextureView,
|
|
10910
|
+
nextRecords: GPUTextureView,
|
|
10911
|
+
swings: GPUTextureView,
|
|
10912
|
+
nextSwings: GPUTextureView,
|
|
10913
|
+
) =>
|
|
10758
10914
|
device.createBindGroup({
|
|
10759
10915
|
label,
|
|
10760
10916
|
layout,
|
|
@@ -10767,23 +10923,108 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10767
10923
|
{ binding: 5, resource: this.postSampler },
|
|
10768
10924
|
{ binding: 6, resource: { buffer: params } },
|
|
10769
10925
|
{ binding: 7, resource: target },
|
|
10926
|
+
{ binding: 8, resource: records },
|
|
10927
|
+
{ binding: 9, resource: nextRecords },
|
|
10928
|
+
{ binding: 10, resource: swings },
|
|
10929
|
+
{ binding: 11, resource: nextSwings },
|
|
10770
10930
|
],
|
|
10771
10931
|
});
|
|
10772
10932
|
for (let write = 0; write < 2; write += 1) {
|
|
10773
10933
|
const read = this.reconHistoryViews[1 - write];
|
|
10774
10934
|
const target = this.reconHistoryViews[write];
|
|
10775
|
-
|
|
10776
|
-
this.
|
|
10935
|
+
const records = this.reconFlickerViews[1 - write];
|
|
10936
|
+
const nextRecords = this.reconFlickerViews[write];
|
|
10937
|
+
const swings = this.reconSwingViews[1 - write];
|
|
10938
|
+
const nextSwings = this.reconSwingViews[write];
|
|
10939
|
+
if (read === null || target === null || records === null || nextRecords === null) continue;
|
|
10940
|
+
if (swings === null || nextSwings === null) continue;
|
|
10941
|
+
this.reconGroups[write] = build(
|
|
10942
|
+
`recon.resolve${String(write)}`,
|
|
10943
|
+
read,
|
|
10944
|
+
target,
|
|
10945
|
+
records,
|
|
10946
|
+
nextRecords,
|
|
10947
|
+
swings,
|
|
10948
|
+
nextSwings,
|
|
10949
|
+
);
|
|
10950
|
+
/* The sharpen reads no record, but one layout serves both dispatches: the same pair, so no
|
|
10951
|
+
texture is both read and written. */
|
|
10777
10952
|
this.reconSharpenGroups[write] = build(
|
|
10778
10953
|
`recon.sharpen${String(write)}`,
|
|
10779
10954
|
target,
|
|
10780
10955
|
this.reconShownView,
|
|
10956
|
+
records,
|
|
10957
|
+
nextRecords,
|
|
10958
|
+
swings,
|
|
10959
|
+
nextSwings,
|
|
10781
10960
|
);
|
|
10782
10961
|
}
|
|
10783
10962
|
/* The composite's own group, over the resolved picture instead of the scene target. */
|
|
10784
10963
|
this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
|
|
10785
10964
|
/* And the scene's, because `ensureComposite` built it before this one existed. */
|
|
10786
10965
|
this.rebuildRushBindGroup();
|
|
10966
|
+
/* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
|
|
10967
|
+
this.rebuildBloomGroups();
|
|
10968
|
+
}
|
|
10969
|
+
|
|
10970
|
+
/** This frame's jitter and temporal frame, decided where its scene camera is bound. See `bindMeshPass`. */
|
|
10971
|
+
private jitterFrame(): void {
|
|
10972
|
+
this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
|
|
10973
|
+
if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
|
|
10974
|
+
this.reconMultisampleSaid = true;
|
|
10975
|
+
console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
|
|
10976
|
+
}
|
|
10977
|
+
if (this.reconstructing) {
|
|
10978
|
+
/*
|
|
10979
|
+
* The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
|
|
10980
|
+
* advanced here: a frame binding its mesh pass twice is one frame of the sequence.
|
|
10981
|
+
*
|
|
10982
|
+
* **The y offset is negated on the way in, and that is not a preference.** This shifts the
|
|
10983
|
+
* *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
|
|
10984
|
+
* every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
|
|
10985
|
+
* arrives on screen with its sign reversed, while x, which nothing negates, arrives as
|
|
10986
|
+
* given. The temporal resolve cannot see this because its shader never reads the offset; the
|
|
10987
|
+
* reconstruction reads it at every texel it unprojects, and a sign error there moves every
|
|
10988
|
+
* sample two jitters away from where the depth says it is.
|
|
10989
|
+
*/
|
|
10990
|
+
this.viewProj = jitterProjection(
|
|
10991
|
+
this.jitteredViewProj,
|
|
10992
|
+
this.correctedViewProj,
|
|
10993
|
+
this.reconJitter[0] as number,
|
|
10994
|
+
-(this.reconJitter[1] as number),
|
|
10995
|
+
this.renderWidth,
|
|
10996
|
+
this.renderHeight,
|
|
10997
|
+
) as Float32Array;
|
|
10998
|
+
this.temporalJittering = false;
|
|
10999
|
+
this.temporalHistoryUsable = false;
|
|
11000
|
+
}
|
|
11001
|
+
this.temporalJittering =
|
|
11002
|
+
!this.reconstructing &&
|
|
11003
|
+
this.quality.temporalAa &&
|
|
11004
|
+
this.quality.screenEffects &&
|
|
11005
|
+
!this.reflectionPassActive;
|
|
11006
|
+
if (this.temporalJittering) {
|
|
11007
|
+
/* The scene's size, not the drawing buffer's: the history is a picture of the render. */
|
|
11008
|
+
this.temporalHistoryUsable = this.temporalHistory.openFrame(
|
|
11009
|
+
this.renderWidth,
|
|
11010
|
+
this.renderHeight,
|
|
11011
|
+
);
|
|
11012
|
+
const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
|
|
11013
|
+
this.temporalJitterX = jx;
|
|
11014
|
+
this.temporalJitterY = jy;
|
|
11015
|
+
/* A jitter is half a *render* texel, which is what the sequence is spread over. */
|
|
11016
|
+
this.viewProj = jitterProjection(
|
|
11017
|
+
this.jitteredViewProj,
|
|
11018
|
+
this.correctedViewProj,
|
|
11019
|
+
jx,
|
|
11020
|
+
jy,
|
|
11021
|
+
this.renderWidth,
|
|
11022
|
+
this.renderHeight,
|
|
11023
|
+
) as Float32Array;
|
|
11024
|
+
} else if (!this.reconstructing) {
|
|
11025
|
+
this.temporalHistoryUsable = false;
|
|
11026
|
+
this.viewProj = this.correctedViewProj;
|
|
11027
|
+
}
|
|
10787
11028
|
}
|
|
10788
11029
|
|
|
10789
11030
|
/**
|
|
@@ -10809,15 +11050,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10809
11050
|
* otherwise. **Late draws record whatever `quality.frameGraph` says**, since the late replay is
|
|
10810
11051
|
* the only moment they can land; everything else keeps the path it has.
|
|
10811
11052
|
*/
|
|
10812
|
-
private recordBlended(reads: number): DrawCommand | null {
|
|
11053
|
+
private recordBlended(reads: number, lands = this.drawsLate()): DrawCommand | null {
|
|
10813
11054
|
const late = this.latePass;
|
|
10814
|
-
if (late !== null &&
|
|
11055
|
+
if (late !== null && lands) return late.take();
|
|
10815
11056
|
return this.recordDraw(reads, this.currentTarget());
|
|
10816
11057
|
}
|
|
10817
11058
|
|
|
10818
11059
|
/** The pipelines a blended draw is built for: the reconstructed picture's while late. */
|
|
10819
|
-
private blendedPipelines(): PipelineCache {
|
|
10820
|
-
return
|
|
11060
|
+
private blendedPipelines(lands = this.drawsLate()): PipelineCache {
|
|
11061
|
+
return lands ? this.latePipelines : this.targetPipelines();
|
|
10821
11062
|
}
|
|
10822
11063
|
|
|
10823
11064
|
/**
|
|
@@ -10835,7 +11076,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10835
11076
|
const width = Math.max(1, this.surface.canvas.width);
|
|
10836
11077
|
const height = Math.max(1, this.surface.canvas.height);
|
|
10837
11078
|
/* The order-independent set tests against this depth too, so it is made for either. */
|
|
10838
|
-
late.upscale(
|
|
11079
|
+
late.upscale(
|
|
11080
|
+
encoder,
|
|
11081
|
+
depth,
|
|
11082
|
+
this.renderWidth,
|
|
11083
|
+
this.renderHeight,
|
|
11084
|
+
width,
|
|
11085
|
+
height,
|
|
11086
|
+
this.reconJitter[0] as number,
|
|
11087
|
+
this.reconJitter[1] as number,
|
|
11088
|
+
);
|
|
10839
11089
|
if (this.lateRefractWanted && this.refractSnapshot !== null && this.reconShown !== null) {
|
|
10840
11090
|
encoder.copyTextureToTexture(
|
|
10841
11091
|
{ texture: this.reconShown },
|
|
@@ -10994,7 +11244,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10994
11244
|
* OpenGL's depth convention — which is the convention the shader's accessor converts the
|
|
10995
11245
|
* renderer's reversed buffer into.
|
|
10996
11246
|
*/
|
|
10997
|
-
|
|
11247
|
+
/*
|
|
11248
|
+
* The inverse and the reprojection in double precision, rounded once on the way into the
|
|
11249
|
+
* uniform: the product of two single-precision matrices this far apart in depth is not the
|
|
11250
|
+
* identity for a still camera, and the resolve read the difference as motion.
|
|
11251
|
+
*/
|
|
11252
|
+
mat4.invert(this.reconInverse64, this.frameRawViewProj);
|
|
11253
|
+
mat4.multiply(this.reconReprojection64, this.reconPreviousRawViewProj, this.reconInverse64);
|
|
11254
|
+
for (let i = 0; i < 16; i += 1) {
|
|
11255
|
+
this.reconInverseViewProj[i] = this.reconInverse64[i] as number;
|
|
11256
|
+
}
|
|
10998
11257
|
const f = this.reconFloats;
|
|
10999
11258
|
const u = this.reconInts;
|
|
11000
11259
|
u[0] = this.renderWidth;
|
|
@@ -11018,6 +11277,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11018
11277
|
f[69] = DEFAULT_DISOCCLUSION.motionScale;
|
|
11019
11278
|
f[70] = DEFAULT_DISOCCLUSION.normalFloor;
|
|
11020
11279
|
f[71] = DEFAULT_DISOCCLUSION.normalCeiling;
|
|
11280
|
+
/* The anti-flicker over the reconstruction's own period, its rule made once a period. */
|
|
11281
|
+
const phases = reconJitterPhases(this.renderWidth, outputWidth);
|
|
11282
|
+
if (phases !== this.reconFlickerPhases) {
|
|
11283
|
+
this.reconFlickerPhases = phases;
|
|
11284
|
+
this.reconFlickerRule = flickerRule(phases);
|
|
11285
|
+
}
|
|
11286
|
+
u[72] = this.reconFrameIndex % phases === 0 ? 1 : 0;
|
|
11287
|
+
u[73] = 1;
|
|
11288
|
+
f[74] = this.reconFlickerRule.memory;
|
|
11289
|
+
f[75] = this.reconFlickerRule.width;
|
|
11290
|
+
f.set(this.reconReprojection64, 76);
|
|
11021
11291
|
device.queue.writeBuffer(params, 0, this.reconStaging);
|
|
11022
11292
|
|
|
11023
11293
|
const groupsX = Math.ceil(outputWidth / RECON_WORKGROUP);
|
|
@@ -12317,6 +12587,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12317
12587
|
command.indexBuffer = null;
|
|
12318
12588
|
command.indexed = false;
|
|
12319
12589
|
command.instances = 1;
|
|
12590
|
+
command.indirect = null;
|
|
12320
12591
|
recordNode(this.arena, VERB_DRAW, reads, writes, at, 0);
|
|
12321
12592
|
return command;
|
|
12322
12593
|
}
|
|
@@ -12363,7 +12634,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12363
12634
|
}
|
|
12364
12635
|
if (command.indexed && command.indexBuffer !== null) {
|
|
12365
12636
|
pass.setIndexBuffer(command.indexBuffer, 'uint32');
|
|
12366
|
-
pass.
|
|
12637
|
+
if (command.indirect !== null) pass.drawIndexedIndirect(command.indirect, 0);
|
|
12638
|
+
else pass.drawIndexed(command.count, command.instances);
|
|
12367
12639
|
} else {
|
|
12368
12640
|
pass.draw(command.count, command.instances);
|
|
12369
12641
|
}
|
|
@@ -13020,6 +13292,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13020
13292
|
* declares nothing behaves exactly as it did before. Read it beside `visible`, which answers the
|
|
13021
13293
|
* other half of the same question.
|
|
13022
13294
|
*/
|
|
13295
|
+
/** Whether a world-space box is wholly behind the declared occluders. See `Renderer.occludedBox`. */
|
|
13296
|
+
occludedBox(min: ArrayLike<number>, max: ArrayLike<number>): boolean {
|
|
13297
|
+
return this.occlusion?.occludedBox(min, max) ?? false;
|
|
13298
|
+
}
|
|
13299
|
+
|
|
13023
13300
|
occluded(bounds: Bounds, model: ReadonlyMat4): boolean {
|
|
13024
13301
|
return this.occlusion?.occluded(bounds, model) ?? false;
|
|
13025
13302
|
}
|
|
@@ -13475,12 +13752,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13475
13752
|
const cssHeight = Math.max(canvas.clientHeight, 1);
|
|
13476
13753
|
const origin = canvas.getBoundingClientRect();
|
|
13477
13754
|
/*
|
|
13478
|
-
* **CSS pixels to the
|
|
13479
|
-
*
|
|
13480
|
-
*
|
|
13755
|
+
* **CSS pixels to the pixels of the attachment this pass draws into.** Mid-frame that is the
|
|
13756
|
+
* scene's render size, which a reconstruction makes smaller than the drawing buffer; after the
|
|
13757
|
+
* present it is the swap image, the drawing buffer itself. Measured against the scene's size
|
|
13758
|
+
* there, an interface's inset landed two thirds of the way to where it was asked for.
|
|
13481
13759
|
*/
|
|
13482
|
-
const
|
|
13483
|
-
const
|
|
13760
|
+
const target = this.passSize();
|
|
13761
|
+
const scaleX = target[0] / cssWidth;
|
|
13762
|
+
const scaleY = target[1] / cssHeight;
|
|
13484
13763
|
const left = rect.left - origin.left;
|
|
13485
13764
|
const top = rect.top - origin.top;
|
|
13486
13765
|
const w = Math.max(1, Math.round(rect.width * scaleX));
|
|
@@ -13508,9 +13787,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13508
13787
|
* edge for the frames it is half-off. That asymmetry is the cost of the clamp, it is
|
|
13509
13788
|
* recorded in the parity ledger, and the alternative was an exception.
|
|
13510
13789
|
*/
|
|
13511
|
-
|
|
13512
|
-
const
|
|
13513
|
-
const targetHeight = this.renderHeight;
|
|
13790
|
+
const targetWidth = target[0];
|
|
13791
|
+
const targetHeight = target[1];
|
|
13514
13792
|
const clampedX = Math.max(0, Math.min(x, targetWidth));
|
|
13515
13793
|
const clampedY = Math.max(0, Math.min(y, targetHeight));
|
|
13516
13794
|
const clampedW = Math.max(0, Math.min(x + w, targetWidth) - clampedX);
|
|
@@ -13587,11 +13865,31 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13587
13865
|
if (this.quality.frameGraph) this.flushGraph();
|
|
13588
13866
|
const pass = this.pass;
|
|
13589
13867
|
if (this.surface.lost || pass === null) return;
|
|
13590
|
-
/* The whole of what this pass draws into
|
|
13591
|
-
|
|
13592
|
-
pass.
|
|
13868
|
+
/* The whole of what this pass draws into: the scene's size mid-frame, the canvas after. */
|
|
13869
|
+
const [width, height] = this.passSize();
|
|
13870
|
+
pass.setViewport(0, 0, width, height, 0, 1);
|
|
13871
|
+
pass.setScissorRect(0, 0, width, height);
|
|
13593
13872
|
}
|
|
13594
13873
|
|
|
13874
|
+
/**
|
|
13875
|
+
* The size of the attachment the open pass draws into: the swap image — the drawing buffer —
|
|
13876
|
+
* in the overlay after the present, the scene's render size in the frame's own pass. A tuple
|
|
13877
|
+
* held once rather than a new array a call.
|
|
13878
|
+
*/
|
|
13879
|
+
private passSize(): readonly [number, number] {
|
|
13880
|
+
const size = this.passSizeOut;
|
|
13881
|
+
if (this.overlayActive) {
|
|
13882
|
+
size[0] = Math.max(1, this.surface.canvas.width);
|
|
13883
|
+
size[1] = Math.max(1, this.surface.canvas.height);
|
|
13884
|
+
} else {
|
|
13885
|
+
size[0] = this.renderWidth;
|
|
13886
|
+
size[1] = this.renderHeight;
|
|
13887
|
+
}
|
|
13888
|
+
return size;
|
|
13889
|
+
}
|
|
13890
|
+
|
|
13891
|
+
private readonly passSizeOut: [number, number] = [0, 0];
|
|
13892
|
+
|
|
13595
13893
|
/**
|
|
13596
13894
|
* Copy a rectangle of the frame into a 2D canvas, pixel for pixel.
|
|
13597
13895
|
*
|
|
@@ -14182,10 +14480,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14182
14480
|
private probeView: GPUTextureView | null = null;
|
|
14183
14481
|
private readonly probeSize: number;
|
|
14184
14482
|
private readonly probeMipLayout: GPUBindGroupLayout;
|
|
14185
|
-
/** The
|
|
14186
|
-
private
|
|
14483
|
+
/** The fixture atlas — cookies over profiles — built on first use. See `fixtureAtlas.ts`. */
|
|
14484
|
+
private fixtureTexture: GPUTexture | null = null;
|
|
14485
|
+
private fixtureView: GPUTextureView | null = null;
|
|
14486
|
+
/** The profiles as packed and the cookie images, kept so either can rebuild the atlas. */
|
|
14487
|
+
private iesAtlas = packIesAtlas([]);
|
|
14488
|
+
private cookieImages: readonly TexImageSource[] = [];
|
|
14187
14489
|
/** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
|
|
14188
|
-
private lightField:
|
|
14490
|
+
private lightField: DriftLightVolumes | null = null;
|
|
14189
14491
|
private driftIndex: GPUTexture | null = null;
|
|
14190
14492
|
private driftAtlas: GPUTexture | null = null;
|
|
14191
14493
|
private driftIndexView: GPUTextureView | null = null;
|
|
@@ -14197,10 +14499,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14197
14499
|
/** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
|
|
14198
14500
|
private readonly bounceLights = createBounceLights();
|
|
14199
14501
|
private readonly bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
|
|
14200
|
-
|
|
14201
|
-
private cookieTexture: GPUTexture | null = null;
|
|
14202
|
-
private cookieView: GPUTextureView | null = null;
|
|
14203
|
-
/** Tiles in the cookie atlas. 0 is the white placeholder and the whole off path. */
|
|
14502
|
+
/** Cookie tiles in the atlas. 0 is none, and the whole off path. */
|
|
14204
14503
|
private cookieTiles = 0;
|
|
14205
14504
|
/** Rows in that atlas. One until a consumer loads a profile, which is the row of ones. */
|
|
14206
14505
|
private iesRows = 1;
|
|
@@ -14688,42 +14987,58 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14688
14987
|
*/
|
|
14689
14988
|
setSpotCookies(images: readonly TexImageSource[]): void {
|
|
14690
14989
|
if (this.surface.lost) return;
|
|
14990
|
+
this.cookieImages = images.slice();
|
|
14991
|
+
this.forgetFixtureAtlas();
|
|
14992
|
+
this.rebuildFlatBindGroup();
|
|
14993
|
+
}
|
|
14994
|
+
|
|
14995
|
+
/** Let the atlas go, to be built again from what is in hand the next time it is bound. */
|
|
14996
|
+
private forgetFixtureAtlas(): void {
|
|
14997
|
+
this.fixtureTexture?.destroy();
|
|
14998
|
+
this.fixtureTexture = null;
|
|
14999
|
+
this.fixtureView = null;
|
|
15000
|
+
}
|
|
15001
|
+
|
|
15002
|
+
/**
|
|
15003
|
+
* The fixture atlas's view, built from the profiles and cookies in hand if it is not already: the
|
|
15004
|
+
* profiles' rows at the bottom, the cookies' tiles on top. See `renderer.ts` and `fixtureAtlas.ts`.
|
|
15005
|
+
*
|
|
15006
|
+
* **`rgba16float`, not `rgba32float`**: a 32-bit float texture is not filterable in core WebGPU,
|
|
15007
|
+
* and binding one to a filtering sampler takes the whole bind group with it. Both tables want
|
|
15008
|
+
* linear filtering. `RENDER_ATTACHMENT` because `copyExternalImageToTexture` writes through one.
|
|
15009
|
+
*/
|
|
15010
|
+
private fixtureAtlasView(): GPUTextureView {
|
|
15011
|
+
if (this.fixtureView !== null) return this.fixtureView;
|
|
14691
15012
|
const { device } = this.surface;
|
|
14692
|
-
const
|
|
14693
|
-
const
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
}
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
{
|
|
14712
|
-
{ width:
|
|
15013
|
+
const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length);
|
|
15014
|
+
const texture = device.createTexture({
|
|
15015
|
+
label: 'fixture.atlas',
|
|
15016
|
+
size: [layout.width, layout.height],
|
|
15017
|
+
format: 'rgba16float',
|
|
15018
|
+
usage: 0x4 | 0x2 | 0x10,
|
|
15019
|
+
});
|
|
15020
|
+
device.queue.writeTexture(
|
|
15021
|
+
{ texture },
|
|
15022
|
+
/* Converted, because `writeTexture` does not — see `environmentTexels`. */
|
|
15023
|
+
toHalfFloats(fixtureAtlasTexels(layout, this.iesAtlas)),
|
|
15024
|
+
{ bytesPerRow: layout.width * 8, rowsPerImage: layout.height },
|
|
15025
|
+
{ width: layout.width, height: layout.height, depthOrArrayLayers: 1 },
|
|
15026
|
+
);
|
|
15027
|
+
for (let tile = 0; tile < this.cookieImages.length; tile++) {
|
|
15028
|
+
const source = this.cookieImages[tile];
|
|
15029
|
+
if (source === undefined) continue;
|
|
15030
|
+
device.queue.copyExternalImageToTexture(
|
|
15031
|
+
{ source: source as GPUCopyExternalImageSource },
|
|
15032
|
+
{ texture, origin: { x: tile * COOKIE_TILE, y: 0 } },
|
|
15033
|
+
{ width: COOKIE_TILE, height: COOKIE_TILE },
|
|
14713
15034
|
);
|
|
14714
|
-
} else {
|
|
14715
|
-
for (let tile = 0; tile < tiles; tile++) {
|
|
14716
|
-
const source = images[tile];
|
|
14717
|
-
if (source === undefined) continue;
|
|
14718
|
-
device.queue.copyExternalImageToTexture(
|
|
14719
|
-
{ source: source as GPUCopyExternalImageSource },
|
|
14720
|
-
{ texture: this.cookieTexture, origin: { x: tile * COOKIE_TILE, y: 0 } },
|
|
14721
|
-
{ width: COOKIE_TILE, height: COOKIE_TILE },
|
|
14722
|
-
);
|
|
14723
|
-
}
|
|
14724
15035
|
}
|
|
14725
|
-
this.
|
|
14726
|
-
this.
|
|
15036
|
+
this.fixtureTexture = texture;
|
|
15037
|
+
this.fixtureView = texture.createView();
|
|
15038
|
+
this.iesRows = this.iesAtlas.height;
|
|
15039
|
+
this.iesPlanes = this.iesAtlas.planes;
|
|
15040
|
+
this.cookieTiles = this.cookieImages.length;
|
|
15041
|
+
return this.fixtureView;
|
|
14727
15042
|
}
|
|
14728
15043
|
|
|
14729
15044
|
/**
|
|
@@ -14740,6 +15055,23 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14740
15055
|
return field;
|
|
14741
15056
|
}
|
|
14742
15057
|
|
|
15058
|
+
/**
|
|
15059
|
+
* Hand a world's DriftLight volume to the renderer: every fixed light of a city, summed offline by
|
|
15060
|
+
* `bakeDenseField`, standing in past the frame's exact choice. Replaces any field made before, of
|
|
15061
|
+
* either kind; the scene calls `field.follow(buffer.complete, ...)` each frame as it would for
|
|
15062
|
+
* `createLightField`, and marks the lights the volume summed `inLightField` so none is counted
|
|
15063
|
+
* twice. See `driftLight/worldLightField.ts`.
|
|
15064
|
+
*/
|
|
15065
|
+
createWorldLightField(
|
|
15066
|
+
volume: DenseLightVolume,
|
|
15067
|
+
options: WorldLightFieldOptions = {},
|
|
15068
|
+
): WorldLightField {
|
|
15069
|
+
this.forgetLightFieldVolumes();
|
|
15070
|
+
const field = new WorldLightField(volume, options);
|
|
15071
|
+
this.lightField = field;
|
|
15072
|
+
return field;
|
|
15073
|
+
}
|
|
15074
|
+
|
|
14743
15075
|
/** Let go of the field and its volumes; the scene's lights are all exact again. */
|
|
14744
15076
|
disposeLightField(): void {
|
|
14745
15077
|
this.forgetLightFieldVolumes();
|
|
@@ -14766,12 +15098,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14766
15098
|
*/
|
|
14767
15099
|
private uploadLightField(): void {
|
|
14768
15100
|
const field = this.lightField;
|
|
14769
|
-
if (field === null || this.driftAtlas !== null || !field.ready || field.
|
|
15101
|
+
if (field === null || this.driftAtlas !== null || !field.ready || field.empty) {
|
|
14770
15102
|
return;
|
|
14771
15103
|
}
|
|
14772
15104
|
if (this.surface.lost) return;
|
|
14773
15105
|
const { device } = this.surface;
|
|
14774
|
-
const [ix, iy, iz] = field.
|
|
15106
|
+
const [ix, iy, iz] = field.indexDims;
|
|
14775
15107
|
const index = device.createTexture({
|
|
14776
15108
|
label: 'driftLight.index',
|
|
14777
15109
|
size: [ix, iy, iz],
|
|
@@ -14781,7 +15113,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14781
15113
|
});
|
|
14782
15114
|
device.queue.writeTexture(
|
|
14783
15115
|
{ texture: index },
|
|
14784
|
-
field.
|
|
15116
|
+
field.index,
|
|
14785
15117
|
{ bytesPerRow: ix * 4, rowsPerImage: iy },
|
|
14786
15118
|
[ix, iy, iz],
|
|
14787
15119
|
);
|
|
@@ -14814,45 +15146,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14814
15146
|
|
|
14815
15147
|
setIesProfiles(profiles: readonly PhotometricProfile[]): void {
|
|
14816
15148
|
if (this.surface.lost) return;
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
/*
|
|
14820
|
-
* Reallocated when the row count changes, because a texture's size is immutable. A set of the
|
|
14821
|
-
* same size overwrites in place, which is the common case of a consumer swapping one fixture.
|
|
14822
|
-
*/
|
|
14823
|
-
if (this.iesTexture === null || this.iesTexture.height !== atlas.height) {
|
|
14824
|
-
this.iesTexture?.destroy();
|
|
14825
|
-
this.iesTexture = device.createTexture({
|
|
14826
|
-
label: 'ies.atlas',
|
|
14827
|
-
size: [atlas.width, atlas.height],
|
|
14828
|
-
/*
|
|
14829
|
-
* **`r16float`, not `r32float`, and that is a filtering constraint rather than a size
|
|
14830
|
-
* choice.** A 32-bit float texture is not filterable in core WebGPU — it needs the
|
|
14831
|
-
* optional `float32-filterable` feature — and binding one to a filtering sampler is a
|
|
14832
|
-
* validation failure that takes the whole bind group with it. This row *wants* linear
|
|
14833
|
-
* filtering: it is a curve sampled between two angles, which is the entire reason it is
|
|
14834
|
-
* not `NEAREST` like the froxel table beside it.
|
|
14835
|
-
*
|
|
14836
|
-
* Half precision holds a normalised intensity to about three decimal digits, which is far
|
|
14837
|
-
* finer than the eight-bit frame it ends up in. **What would make it wrong** is a profile
|
|
14838
|
-
* carrying absolute candela rather than a normalised curve, which would run out of range —
|
|
14839
|
-
* and `packIesAtlas` normalises for its own reasons.
|
|
14840
|
-
*/
|
|
14841
|
-
format: 'r16float',
|
|
14842
|
-
usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
|
|
14843
|
-
});
|
|
14844
|
-
this.iesView = this.iesTexture.createView();
|
|
14845
|
-
}
|
|
14846
|
-
device.queue.writeTexture(
|
|
14847
|
-
{ texture: this.iesTexture },
|
|
14848
|
-
/* Converted, because `writeTexture` does not — see `environmentTexels` for what handing it
|
|
14849
|
-
the wrong width silently produces. */
|
|
14850
|
-
toHalfFloats(atlas.data),
|
|
14851
|
-
{ bytesPerRow: atlas.width * 2, rowsPerImage: atlas.height },
|
|
14852
|
-
{ width: atlas.width, height: atlas.height, depthOrArrayLayers: 1 },
|
|
14853
|
-
);
|
|
14854
|
-
this.iesRows = atlas.height;
|
|
14855
|
-
this.iesPlanes = atlas.planes;
|
|
15149
|
+
this.iesAtlas = packIesAtlas(profiles);
|
|
15150
|
+
this.forgetFixtureAtlas();
|
|
14856
15151
|
this.rebuildFlatBindGroup();
|
|
14857
15152
|
}
|
|
14858
15153
|
|
|
@@ -15356,7 +15651,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
15356
15651
|
* An empty list is a real answer for a frame too small to hold one, and `runBloom` reads it
|
|
15357
15652
|
* as no bloom rather than as a target it should have had.
|
|
15358
15653
|
*/
|
|
15359
|
-
|
|
15654
|
+
/* The resolved picture's size while reconstructing, as the order-independent pair above is. */
|
|
15655
|
+
this.bloomSourceWidth = oitSize[0];
|
|
15656
|
+
this.bloomSourceHeight = oitSize[1];
|
|
15657
|
+
this.bloomLevels = bloomLevelSizes(oitSize[0], oitSize[1]).map((size, index) => {
|
|
15360
15658
|
const texture = device.createTexture({
|
|
15361
15659
|
label: `post.bloom${index}`,
|
|
15362
15660
|
size: [size.width, size.height],
|
|
@@ -15529,13 +15827,31 @@ export class WebGPURenderer implements RendererApi {
|
|
|
15529
15827
|
/* The volume's group holds the depth snapshot, which is one of the targets just replaced.
|
|
15530
15828
|
Keeping the old group would clamp every beam against a destroyed texture. */
|
|
15531
15829
|
this.lightVolumeBindGroup = this.buildLightVolumeBindGroup();
|
|
15532
|
-
|
|
15533
|
-
|
|
15534
|
-
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15830
|
+
this.rebuildBloomGroups();
|
|
15831
|
+
this.writeBloomBlocks();
|
|
15832
|
+
}
|
|
15833
|
+
|
|
15834
|
+
/**
|
|
15835
|
+
* One group per source the bloom chain reads, in the order `runBloom` indexes them: the picture
|
|
15836
|
+
* first, then every level. The prefilter reads the picture; the downsample into level `i` reads
|
|
15837
|
+
* level `i - 1`, which is group `i`; the upsample into level `i - 1` reads level `i`, which is
|
|
15838
|
+
* group `i + 1`.
|
|
15839
|
+
*
|
|
15840
|
+
* **While reconstructing the picture is the resolved one**, which `ensureReconstruction` makes
|
|
15841
|
+
* after this runs and rebuilds these over. The scene target is the jittered render then: a bright
|
|
15842
|
+
* thing smaller than a render texel is caught in some phases and missed in others, and its halo,
|
|
15843
|
+
* wide and over everything round it, flashed with it — a night street's panels breathing a few
|
|
15844
|
+
* levels of luma frame to frame. Built at the drawing buffer's size, which `ensureComposite`
|
|
15845
|
+
* already sized the pyramid for.
|
|
15846
|
+
*/
|
|
15847
|
+
private rebuildBloomGroups(): void {
|
|
15848
|
+
const picture =
|
|
15849
|
+
this.reconstructionWanted && this.reconShownView !== null
|
|
15850
|
+
? this.reconShownView
|
|
15851
|
+
: this.sceneColorView;
|
|
15852
|
+
if (picture === null) return;
|
|
15853
|
+
const { device } = this.surface;
|
|
15854
|
+
this.bloomGroups = [picture, ...this.bloomLevels.map((level) => level.view)].map(
|
|
15539
15855
|
(view, index) =>
|
|
15540
15856
|
device.createBindGroup({
|
|
15541
15857
|
label: `post.bloomGroup${index}`,
|
|
@@ -15545,11 +15861,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
15545
15861
|
binding: BLOOM_UNIFORMS,
|
|
15546
15862
|
resource: { buffer: this.bloomUniforms, size: BLOOM_STAGE_SIZE },
|
|
15547
15863
|
},
|
|
15548
|
-
|
|
15864
|
+
{ binding: BLOOM_TEXTURES.uSource.texture, resource: view },
|
|
15865
|
+
{ binding: BLOOM_TEXTURES.uSource.sampler, resource: this.postSampler },
|
|
15549
15866
|
],
|
|
15550
15867
|
}),
|
|
15551
15868
|
);
|
|
15552
|
-
this.writeBloomBlocks();
|
|
15553
15869
|
}
|
|
15554
15870
|
|
|
15555
15871
|
/**
|
|
@@ -15567,12 +15883,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
15567
15883
|
if (levels.length === 0) return;
|
|
15568
15884
|
const slot = (index: number): number => (index * BLOOM_SLOT) / 4;
|
|
15569
15885
|
|
|
15570
|
-
/* The prefilter reads the *
|
|
15886
|
+
/* The prefilter reads the *picture*, so its texel is the picture's rather than a level's.
|
|
15571
15887
|
`uTexel`, not `uTexelSize`, and the block carries no strength at all: the composite
|
|
15572
15888
|
applies that when it adds the result back. Both were written wrong here first. */
|
|
15573
15889
|
const prefilter = (name: string): number => this.postField(BLOOM_PREFILTER_FIELDS, name);
|
|
15574
|
-
f[slot(0) + prefilter('uTexel')] = 1 / Math.max(1, this.
|
|
15575
|
-
f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.
|
|
15890
|
+
f[slot(0) + prefilter('uTexel')] = 1 / Math.max(1, this.bloomSourceWidth);
|
|
15891
|
+
f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.bloomSourceHeight);
|
|
15576
15892
|
/* In scene units, which is why an HDR target is what makes a threshold above 1 mean
|
|
15577
15893
|
anything: against a clamped buffer nothing is ever brighter than white. */
|
|
15578
15894
|
f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
|
|
@@ -15699,6 +16015,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
15699
16015
|
this.fieldComposer = null;
|
|
15700
16016
|
this.driftIndex?.destroy();
|
|
15701
16017
|
this.driftAtlas?.destroy();
|
|
16018
|
+
this.forgetFixtureAtlas();
|
|
15702
16019
|
this.sunTint?.texture.destroy();
|
|
15703
16020
|
this.sunTint = null;
|
|
15704
16021
|
this.glassTint?.dispose();
|