@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
|
@@ -142,7 +142,12 @@ export function createSdfTextBindGroupLayout(device: GPUDevice): GPUBindGroupLay
|
|
|
142
142
|
visibility: VISIBILITY_FRAGMENT,
|
|
143
143
|
buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: SDF_TEXT_FRAG_SIZE },
|
|
144
144
|
},
|
|
145
|
-
|
|
145
|
+
/* The atlas is a surface texture, and every surface texture is a `2d-array` view. */
|
|
146
|
+
{
|
|
147
|
+
binding: ATLAS.texture,
|
|
148
|
+
visibility: VISIBILITY_FRAGMENT,
|
|
149
|
+
texture: { viewDimension: '2d-array' },
|
|
150
|
+
},
|
|
146
151
|
{ binding: ATLAS.sampler, visibility: VISIBILITY_FRAGMENT, sampler: {} },
|
|
147
152
|
],
|
|
148
153
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { INSTANCE_FLOATS } from '../../../instances.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The instance cull, in WGSL, hand-authored under the compute exception `AGENTS.md` records
|
|
5
|
+
* (2026-08-24): a compute shader has no WebGL2 twin to generate from, and `npm run wgsl` neither
|
|
6
|
+
* writes nor polices this file.
|
|
7
|
+
*
|
|
8
|
+
* **It keeps exactly what `cullInstances` keeps**, and `gpu-parity.mjs` runs both on a device: an
|
|
9
|
+
* instance's world sphere is the mesh's sphere with its centre through the model matrix and its
|
|
10
|
+
* radius scaled by the largest column, and it is kept unless wholly beyond one of the six planes.
|
|
11
|
+
* The one difference is order — survivors are compacted by `atomicAdd`, so they arrive in whatever
|
|
12
|
+
* order the invocations reached it. That is invisible to an opaque draw and is why a blended draw
|
|
13
|
+
* is never culled here.
|
|
14
|
+
*
|
|
15
|
+
* `args` is a `drawIndexedIndirect` record, `(indexCount, instanceCount, firstIndex, baseVertex,
|
|
16
|
+
* firstInstance)`; the renderer writes it with a zero instance count before each dispatch and this
|
|
17
|
+
* counts the survivors into it.
|
|
18
|
+
*/
|
|
19
|
+
export const INSTANCE_CULL_WORKGROUP = 64;
|
|
20
|
+
|
|
21
|
+
export const INSTANCE_CULL_WGSL = /* wgsl */ `
|
|
22
|
+
struct Params {
|
|
23
|
+
planes: array<vec4<f32>, 6>,
|
|
24
|
+
sphere: vec4<f32>,
|
|
25
|
+
count: u32,
|
|
26
|
+
pad0: u32,
|
|
27
|
+
pad1: u32,
|
|
28
|
+
pad2: u32,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
32
|
+
@group(0) @binding(1) var<storage, read> source: array<f32>;
|
|
33
|
+
@group(0) @binding(2) var<storage, read_write> kept: array<f32>;
|
|
34
|
+
@group(0) @binding(3) var<storage, read_write> args: array<atomic<u32>, 5>;
|
|
35
|
+
|
|
36
|
+
const STRIDE: u32 = ${INSTANCE_FLOATS}u;
|
|
37
|
+
|
|
38
|
+
@compute @workgroup_size(${INSTANCE_CULL_WORKGROUP})
|
|
39
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
40
|
+
let i = id.x;
|
|
41
|
+
if (i >= params.count) { return; }
|
|
42
|
+
let at = i * STRIDE;
|
|
43
|
+
let c0 = vec3<f32>(source[at], source[at + 1u], source[at + 2u]);
|
|
44
|
+
let c1 = vec3<f32>(source[at + 4u], source[at + 5u], source[at + 6u]);
|
|
45
|
+
let c2 = vec3<f32>(source[at + 8u], source[at + 9u], source[at + 10u]);
|
|
46
|
+
let t = vec3<f32>(source[at + 12u], source[at + 13u], source[at + 14u]);
|
|
47
|
+
let local = params.sphere;
|
|
48
|
+
let centre = c0 * local.x + c1 * local.y + c2 * local.z + t;
|
|
49
|
+
let scale = sqrt(max(dot(c0, c0), max(dot(c1, c1), dot(c2, c2))));
|
|
50
|
+
let radius = local.w * scale;
|
|
51
|
+
for (var p = 0u; p < 6u; p = p + 1u) {
|
|
52
|
+
let plane = params.planes[p];
|
|
53
|
+
if (dot(plane.xyz, centre) + plane.w < -radius) { return; }
|
|
54
|
+
}
|
|
55
|
+
let slot = atomicAdd(&args[1], 1u);
|
|
56
|
+
let to = slot * STRIDE;
|
|
57
|
+
for (var k = 0u; k < STRIDE; k = k + 1u) {
|
|
58
|
+
kept[to + k] = source[at + k];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { SurfaceTextureOptions } from '../../surfaceTexture.ts';
|
|
2
|
+
import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from '../../surfaceEffects.ts';
|
|
3
|
+
import { toHalfFloats } from '../../halfFloat.ts';
|
|
4
|
+
import { isSourceList, layerSize, refuseArrayUpdate, sourceSize } from '../../textureSource.ts';
|
|
2
5
|
import type { PipelineCache } from './pipelineCache.ts';
|
|
3
6
|
import { shaderModule } from './shaderModules.ts';
|
|
4
7
|
|
|
@@ -89,6 +92,8 @@ export function generateMipChain(
|
|
|
89
92
|
texture: GPUTexture,
|
|
90
93
|
format: GPUTextureFormat,
|
|
91
94
|
levels: number,
|
|
95
|
+
/** How many array layers to fill, each its own chain. One for a plain texture. */
|
|
96
|
+
layers = 1,
|
|
92
97
|
): void {
|
|
93
98
|
if (levels < 2) return;
|
|
94
99
|
const pipeline = mipBlitPipeline(device, pipelines, format);
|
|
@@ -98,29 +103,33 @@ export function generateMipChain(
|
|
|
98
103
|
minFilter: 'linear',
|
|
99
104
|
});
|
|
100
105
|
const encoder = device.createCommandEncoder({ label: 'mip.chain' });
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
0,
|
|
113
|
-
device.createBindGroup({
|
|
114
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
115
|
-
entries: [
|
|
116
|
-
{ binding: 0, resource: source },
|
|
117
|
-
{ binding: 1, resource: sampler },
|
|
106
|
+
/* One level of one layer at a time: a render target is a single 2D subresource, and the source
|
|
107
|
+
view is one level of the same layer, so no pass reads what it writes. */
|
|
108
|
+
for (let layer = 0; layer < layers; layer++)
|
|
109
|
+
for (let level = 1; level < levels; level++) {
|
|
110
|
+
const one = { dimension: '2d' as const, baseArrayLayer: layer, arrayLayerCount: 1 };
|
|
111
|
+
const source = texture.createView({ ...one, baseMipLevel: level - 1, mipLevelCount: 1 });
|
|
112
|
+
const target = texture.createView({ ...one, baseMipLevel: level, mipLevelCount: 1 });
|
|
113
|
+
const pass = encoder.beginRenderPass({
|
|
114
|
+
label: `mip.level${level}`,
|
|
115
|
+
colorAttachments: [
|
|
116
|
+
{ view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
|
|
118
117
|
],
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
});
|
|
119
|
+
pass.setPipeline(pipeline);
|
|
120
|
+
pass.setBindGroup(
|
|
121
|
+
0,
|
|
122
|
+
device.createBindGroup({
|
|
123
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
124
|
+
entries: [
|
|
125
|
+
{ binding: 0, resource: source },
|
|
126
|
+
{ binding: 1, resource: sampler },
|
|
127
|
+
],
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
pass.draw(3);
|
|
131
|
+
pass.end();
|
|
132
|
+
}
|
|
124
133
|
device.queue.submit([encoder.finish()]);
|
|
125
134
|
}
|
|
126
135
|
|
|
@@ -149,19 +158,11 @@ export function mipBlitPipeline(
|
|
|
149
158
|
}));
|
|
150
159
|
}
|
|
151
160
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
videoHeight?: number;
|
|
158
|
-
};
|
|
159
|
-
const width = candidate.videoWidth ?? candidate.width ?? 0;
|
|
160
|
-
const height = candidate.videoHeight ?? candidate.height ?? 0;
|
|
161
|
-
return { width: Math.max(1, width), height: Math.max(1, height) };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** An image uploaded once and bound per material. */
|
|
161
|
+
/**
|
|
162
|
+
* An image uploaded once and bound per material — or several of one size, as an array's layers.
|
|
163
|
+
* Every surface texture is a `2d-array` view, a plain image an array of one; `surfaceTexture.ts`
|
|
164
|
+
* says why.
|
|
165
|
+
*/
|
|
165
166
|
export class GpuSurfaceTexture {
|
|
166
167
|
readonly sampler: GPUSampler;
|
|
167
168
|
|
|
@@ -172,14 +173,22 @@ export class GpuSurfaceTexture {
|
|
|
172
173
|
private levels: number;
|
|
173
174
|
private width: number;
|
|
174
175
|
private height: number;
|
|
176
|
+
/** How many images the array holds. */
|
|
177
|
+
readonly layers: number;
|
|
178
|
+
/** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
|
|
179
|
+
private effectsTable: GPUTexture | null = null;
|
|
180
|
+
/** Its view, which the flat group binds beside this image when it is a material's albedo. */
|
|
181
|
+
effectsView: GPUTextureView | null = null;
|
|
175
182
|
|
|
176
183
|
constructor(
|
|
177
184
|
private readonly device: GPUDevice,
|
|
178
185
|
private readonly pipelines: PipelineCache,
|
|
179
|
-
source: TexImageSource,
|
|
186
|
+
source: TexImageSource | readonly TexImageSource[],
|
|
180
187
|
options: SurfaceTextureOptions = {},
|
|
181
188
|
) {
|
|
182
|
-
const
|
|
189
|
+
const sources: readonly TexImageSource[] = isSourceList(source) ? source : [source];
|
|
190
|
+
const { width, height } = layerSize(sources);
|
|
191
|
+
this.layers = sources.length;
|
|
183
192
|
this.mipmapped = options.mipmap ?? true;
|
|
184
193
|
/* `SRGB8_ALPHA8`'s equivalent. Decoded in the sampler, before filtering, which is the only
|
|
185
194
|
place it is correct — `surfaceTexture.ts` makes the argument in full. */
|
|
@@ -188,7 +197,7 @@ export class GpuSurfaceTexture {
|
|
|
188
197
|
this.height = height;
|
|
189
198
|
this.levels = this.mipmapped ? mipLevelCount(width, height) : 1;
|
|
190
199
|
this.texture = this.allocate();
|
|
191
|
-
this.current = this.texture
|
|
200
|
+
this.current = this.arrayView(this.texture);
|
|
192
201
|
|
|
193
202
|
const wrap: GPUAddressMode =
|
|
194
203
|
(options.wrap ?? 'repeat') === 'repeat' ? 'repeat' : 'clamp-to-edge';
|
|
@@ -223,7 +232,32 @@ export class GpuSurfaceTexture {
|
|
|
223
232
|
: 1,
|
|
224
233
|
});
|
|
225
234
|
|
|
226
|
-
this.upload(
|
|
235
|
+
this.upload(sources);
|
|
236
|
+
|
|
237
|
+
/* Half floats, as `renderer.ts` uploads it: filterable, so the ordinary float layout takes it,
|
|
238
|
+
and read by `textureLoad` alone, so the filter never runs. */
|
|
239
|
+
const effects = options.effects ?? [];
|
|
240
|
+
if (effects.length > 0) {
|
|
241
|
+
const table = device.createTexture({
|
|
242
|
+
label: 'surface.effects',
|
|
243
|
+
size: [SURFACE_EFFECT_TEXELS, effects.length],
|
|
244
|
+
format: 'rgba16float',
|
|
245
|
+
usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
|
|
246
|
+
});
|
|
247
|
+
device.queue.writeTexture(
|
|
248
|
+
{ texture: table },
|
|
249
|
+
toHalfFloats(packSurfaceEffects(effects)),
|
|
250
|
+
{ bytesPerRow: SURFACE_EFFECT_TEXELS * 8, rowsPerImage: effects.length },
|
|
251
|
+
{ width: SURFACE_EFFECT_TEXELS, height: effects.length, depthOrArrayLayers: 1 },
|
|
252
|
+
);
|
|
253
|
+
this.effectsTable = table;
|
|
254
|
+
this.effectsView = table.createView();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A view of every layer, as the shaders' `texture_2d_array` bindings read it. */
|
|
259
|
+
private arrayView(texture: GPUTexture): GPUTextureView {
|
|
260
|
+
return texture.createView({ dimension: '2d-array', arrayLayerCount: this.layers });
|
|
227
261
|
}
|
|
228
262
|
|
|
229
263
|
/** The view every binding of this image holds. A new one when `update` changes the size. */
|
|
@@ -248,24 +282,25 @@ export class GpuSurfaceTexture {
|
|
|
248
282
|
update(source: TexImageSource): GPUTexture | null {
|
|
249
283
|
const replaced = this.texture;
|
|
250
284
|
if (replaced === null) return null;
|
|
285
|
+
refuseArrayUpdate(this.layers);
|
|
251
286
|
const { width, height } = sourceSize(source);
|
|
252
287
|
if (width === this.width && height === this.height) {
|
|
253
|
-
this.upload(source);
|
|
288
|
+
this.upload([source]);
|
|
254
289
|
return null;
|
|
255
290
|
}
|
|
256
291
|
this.width = width;
|
|
257
292
|
this.height = height;
|
|
258
293
|
this.levels = this.mipmapped ? mipLevelCount(width, height) : 1;
|
|
259
294
|
this.texture = this.allocate();
|
|
260
|
-
this.current = this.texture
|
|
261
|
-
this.upload(source);
|
|
295
|
+
this.current = this.arrayView(this.texture);
|
|
296
|
+
this.upload([source]);
|
|
262
297
|
return replaced;
|
|
263
298
|
}
|
|
264
299
|
|
|
265
300
|
private allocate(): GPUTexture {
|
|
266
301
|
return this.device.createTexture({
|
|
267
302
|
label: 'surface.texture',
|
|
268
|
-
size: [this.width, this.height],
|
|
303
|
+
size: [this.width, this.height, this.layers],
|
|
269
304
|
format: this.format,
|
|
270
305
|
mipLevelCount: this.levels,
|
|
271
306
|
/*
|
|
@@ -290,9 +325,12 @@ export class GpuSurfaceTexture {
|
|
|
290
325
|
if (this.texture === null) return;
|
|
291
326
|
this.texture.destroy();
|
|
292
327
|
this.texture = null;
|
|
328
|
+
this.effectsTable?.destroy();
|
|
329
|
+
this.effectsTable = null;
|
|
330
|
+
this.effectsView = null;
|
|
293
331
|
}
|
|
294
332
|
|
|
295
|
-
private upload(
|
|
333
|
+
private upload(sources: readonly TexImageSource[]): void {
|
|
296
334
|
const texture = this.texture;
|
|
297
335
|
if (texture === null) return;
|
|
298
336
|
/*
|
|
@@ -306,11 +344,13 @@ export class GpuSurfaceTexture {
|
|
|
306
344
|
* The orientation check that passed this backend was run on a canvas alone, where the two
|
|
307
345
|
* agreed. Measure both source types, or a texture pipeline is half tested.
|
|
308
346
|
*/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
347
|
+
for (let layer = 0; layer < sources.length; layer++) {
|
|
348
|
+
this.device.queue.copyExternalImageToTexture(
|
|
349
|
+
{ source: sources[layer] as GPUCopyExternalImageSource, flipY: false },
|
|
350
|
+
{ texture, origin: [0, 0, layer] },
|
|
351
|
+
[this.width, this.height],
|
|
352
|
+
);
|
|
353
|
+
}
|
|
314
354
|
if (this.levels > 1) this.generateMips(texture);
|
|
315
355
|
}
|
|
316
356
|
|
|
@@ -322,6 +362,6 @@ export class GpuSurfaceTexture {
|
|
|
322
362
|
* an attachment and a resource at once — rejected, and rejected silently enough to matter.
|
|
323
363
|
*/
|
|
324
364
|
private generateMips(texture: GPUTexture): void {
|
|
325
|
-
generateMipChain(this.device, this.pipelines, texture, this.format, this.levels);
|
|
365
|
+
generateMipChain(this.device, this.pipelines, texture, this.format, this.levels, this.layers);
|
|
326
366
|
}
|
|
327
367
|
}
|
|
@@ -57,69 +57,92 @@ export function bakeBrick(
|
|
|
57
57
|
for (let k = 0; k < BRICK_SAMPLES; k++) {
|
|
58
58
|
for (let j = 0; j < BRICK_SAMPLES; j++) {
|
|
59
59
|
for (let i = 0; i < BRICK_SAMPLES; i++) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let weight = 0;
|
|
75
|
-
for (let listed = first; listed < last; listed++) {
|
|
76
|
-
const source = lights[layout.lights[listed] as number];
|
|
77
|
-
if (source === undefined) continue;
|
|
78
|
-
const dx = source.x - px;
|
|
79
|
-
const dy = source.y - py;
|
|
80
|
-
const dz = source.z - pz;
|
|
81
|
-
const dist = Math.hypot(dx, dy, dz);
|
|
82
|
-
if (dist >= source.radius) continue;
|
|
83
|
-
const shape = pointLightShape(dist, source.radius, falloff);
|
|
84
|
-
if (shape <= 0) continue;
|
|
85
|
-
const seen =
|
|
86
|
-
distance === null
|
|
87
|
-
? 1
|
|
88
|
-
: softVisibility(
|
|
89
|
-
distance,
|
|
90
|
-
px,
|
|
91
|
-
py,
|
|
92
|
-
pz,
|
|
93
|
-
source.x,
|
|
94
|
-
source.y,
|
|
95
|
-
source.z,
|
|
96
|
-
source.sourceRadius,
|
|
97
|
-
);
|
|
98
|
-
if (seen <= 0) continue;
|
|
99
|
-
const lr = source.r * shape * seen;
|
|
100
|
-
const lg = source.g * shape * seen;
|
|
101
|
-
const lb = source.b * shape * seen;
|
|
102
|
-
r += lr;
|
|
103
|
-
g += lg;
|
|
104
|
-
b += lb;
|
|
105
|
-
const bright = 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
106
|
-
if (dist > 1e-6 && bright > 0) {
|
|
107
|
-
towardX += (bright * dx) / dist;
|
|
108
|
-
towardY += (bright * dy) / dist;
|
|
109
|
-
towardZ += (bright * dz) / dist;
|
|
110
|
-
weight += bright;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
light[texel] = r;
|
|
114
|
-
light[texel + 1] = g;
|
|
115
|
-
light[texel + 2] = b;
|
|
116
|
-
light[texel + 3] = 1;
|
|
117
|
-
if (weight > 0) {
|
|
118
|
-
direction[texel] = towardX / weight;
|
|
119
|
-
direction[texel + 1] = towardY / weight;
|
|
120
|
-
direction[texel + 2] = towardZ / weight;
|
|
121
|
-
}
|
|
60
|
+
sampleFieldLight(
|
|
61
|
+
bx + i * spacing,
|
|
62
|
+
by + j * spacing,
|
|
63
|
+
bz + k * spacing,
|
|
64
|
+
lights,
|
|
65
|
+
layout.lights,
|
|
66
|
+
first,
|
|
67
|
+
last,
|
|
68
|
+
falloff,
|
|
69
|
+
distance,
|
|
70
|
+
light,
|
|
71
|
+
direction,
|
|
72
|
+
(i + BRICK_SAMPLES * (j + BRICK_SAMPLES * k)) * 4,
|
|
73
|
+
);
|
|
122
74
|
}
|
|
123
75
|
}
|
|
124
76
|
}
|
|
125
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* One sample: the light arriving at (px, py, pz) from `lights[candidates[first..last)]`, written at
|
|
81
|
+
* `texel` in `light` (colour, then 1 for a sample and 0 for one inside solid geometry) and in
|
|
82
|
+
* `direction` (the brightness-weighted way it arrives from). Shared by every bake, so a brick and a
|
|
83
|
+
* dense volume cannot come to disagree about what a sample is.
|
|
84
|
+
*/
|
|
85
|
+
export function sampleFieldLight(
|
|
86
|
+
px: number,
|
|
87
|
+
py: number,
|
|
88
|
+
pz: number,
|
|
89
|
+
lights: readonly FieldLight[],
|
|
90
|
+
candidates: ArrayLike<number>,
|
|
91
|
+
first: number,
|
|
92
|
+
last: number,
|
|
93
|
+
falloff: PointLightFalloff,
|
|
94
|
+
distance: DistanceAt | null,
|
|
95
|
+
light: Float32Array,
|
|
96
|
+
direction: Float32Array,
|
|
97
|
+
texel: number,
|
|
98
|
+
): void {
|
|
99
|
+
light.fill(0, texel, texel + 4);
|
|
100
|
+
direction.fill(0, texel, texel + 4);
|
|
101
|
+
if (distance !== null && distance(px, py, pz) < 0) return;
|
|
102
|
+
|
|
103
|
+
let r = 0;
|
|
104
|
+
let g = 0;
|
|
105
|
+
let b = 0;
|
|
106
|
+
let towardX = 0;
|
|
107
|
+
let towardY = 0;
|
|
108
|
+
let towardZ = 0;
|
|
109
|
+
let weight = 0;
|
|
110
|
+
for (let listed = first; listed < last; listed++) {
|
|
111
|
+
const source = lights[candidates[listed] as number];
|
|
112
|
+
if (source === undefined) continue;
|
|
113
|
+
const dx = source.x - px;
|
|
114
|
+
const dy = source.y - py;
|
|
115
|
+
const dz = source.z - pz;
|
|
116
|
+
const dist = Math.hypot(dx, dy, dz);
|
|
117
|
+
if (dist >= source.radius) continue;
|
|
118
|
+
const shape = pointLightShape(dist, source.radius, falloff);
|
|
119
|
+
if (shape <= 0) continue;
|
|
120
|
+
const seen =
|
|
121
|
+
distance === null
|
|
122
|
+
? 1
|
|
123
|
+
: softVisibility(distance, px, py, pz, source.x, source.y, source.z, source.sourceRadius);
|
|
124
|
+
if (seen <= 0) continue;
|
|
125
|
+
const lr = source.r * shape * seen;
|
|
126
|
+
const lg = source.g * shape * seen;
|
|
127
|
+
const lb = source.b * shape * seen;
|
|
128
|
+
r += lr;
|
|
129
|
+
g += lg;
|
|
130
|
+
b += lb;
|
|
131
|
+
const bright = 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
132
|
+
if (dist > 1e-6 && bright > 0) {
|
|
133
|
+
towardX += (bright * dx) / dist;
|
|
134
|
+
towardY += (bright * dy) / dist;
|
|
135
|
+
towardZ += (bright * dz) / dist;
|
|
136
|
+
weight += bright;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
light[texel] = r;
|
|
140
|
+
light[texel + 1] = g;
|
|
141
|
+
light[texel + 2] = b;
|
|
142
|
+
light[texel + 3] = 1;
|
|
143
|
+
if (weight > 0) {
|
|
144
|
+
direction[texel] = towardX / weight;
|
|
145
|
+
direction[texel + 1] = towardY / weight;
|
|
146
|
+
direction[texel + 2] = towardZ / weight;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DriftLight for a whole world: every fixed light summed into one dense grid, baked offline.
|
|
3
|
+
*
|
|
4
|
+
* **Dense, where the courtyard field is sparse, because a city is lit nearly everywhere.** A brick
|
|
5
|
+
* earns its index and its shared faces where light is rare; street lamps reaching thirty metres
|
|
6
|
+
* forty metres apart leave almost no cell dark, so the sparseness buys nothing and the shared
|
|
7
|
+
* faces cost 2.4× the samples. At 8 m a sample, a two-kilometre city 128 m tall is 256 × 16 × 256
|
|
8
|
+
* samples, two half-float texels each: 16 MB on the GPU, and the renderer's lookup is one filtered
|
|
9
|
+
* fetch for the light and one for its direction.
|
|
10
|
+
*
|
|
11
|
+
* **The same sample as every other bake.** `sampleFieldLight` is shared with `bakeBrick`, so a dense
|
|
12
|
+
* volume and a brick agree bit for bit wherever both have a sample. The order a sample's lights are
|
|
13
|
+
* summed in does not have to match for that: the sum runs in double precision and is stored in
|
|
14
|
+
* single, which the last bits of a few terms in another order do not reach.
|
|
15
|
+
*
|
|
16
|
+
* **Lights are bucketed first**, four samples a bucket on every axis, so a sample sums only the
|
|
17
|
+
* lights whose reach touches its bucket rather than all of them: a city of ten thousand lamps is a
|
|
18
|
+
* few dozen candidates a sample. Offline and allocation-free per sample; it may take seconds.
|
|
19
|
+
*
|
|
20
|
+
* What it gives up is resolution: at 8 m a lamp's pool is a few samples across, which is why this
|
|
21
|
+
* stands in only past the exact lights, where a pool is a few dozen pixels.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { FieldLight } from './bake.ts';
|
|
25
|
+
import { sampleFieldLight } from './bake.ts';
|
|
26
|
+
import type { PointLightFalloff } from './falloff.ts';
|
|
27
|
+
import type { DistanceAt } from './visibility.ts';
|
|
28
|
+
|
|
29
|
+
/** The most samples along x or z: WebGL2 promises 256 on every axis of a 3D texture. */
|
|
30
|
+
export const DENSE_MAX_AXIS = 256;
|
|
31
|
+
/** The most along y, which holds the light and then the direction: two layers in 256. */
|
|
32
|
+
export const DENSE_MAX_HEIGHT = 128;
|
|
33
|
+
/** Samples a bucket spans on each axis. */
|
|
34
|
+
const BUCKET = 4;
|
|
35
|
+
|
|
36
|
+
export interface DenseLightVolume {
|
|
37
|
+
/** The world position of sample (0, 0, 0). */
|
|
38
|
+
readonly origin: readonly [number, number, number];
|
|
39
|
+
/** Metres between two samples. */
|
|
40
|
+
readonly spacing: number;
|
|
41
|
+
/** Samples on each axis. */
|
|
42
|
+
readonly dims: readonly [number, number, number];
|
|
43
|
+
/** Four floats a sample, x fastest then y then z: the colour, then 1 for a sample and 0 inside. */
|
|
44
|
+
readonly light: Float32Array;
|
|
45
|
+
/** Four floats a sample: the direction light arrives from, weighted by brightness, then 0. */
|
|
46
|
+
readonly direction: Float32Array;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sum `lights` over the box `bounds` (min xyz, max xyz) at `spacing` metres. `distance` occludes, or
|
|
51
|
+
* null for none. The first sample stands on the box's min corner.
|
|
52
|
+
*/
|
|
53
|
+
export function bakeDenseField(
|
|
54
|
+
lights: readonly FieldLight[],
|
|
55
|
+
falloff: PointLightFalloff,
|
|
56
|
+
distance: DistanceAt | null,
|
|
57
|
+
bounds: ArrayLike<number>,
|
|
58
|
+
spacing: number,
|
|
59
|
+
): DenseLightVolume {
|
|
60
|
+
if (!(spacing > 0)) throw new Error(`bakeDenseField: spacing is ${String(spacing)}`);
|
|
61
|
+
const origin: [number, number, number] = [
|
|
62
|
+
bounds[0] as number,
|
|
63
|
+
bounds[1] as number,
|
|
64
|
+
bounds[2] as number,
|
|
65
|
+
];
|
|
66
|
+
const dims: [number, number, number] = [0, 0, 0];
|
|
67
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
68
|
+
const extent = (bounds[axis + 3] as number) - (origin[axis] as number);
|
|
69
|
+
dims[axis] = Math.max(1, Math.floor(extent / spacing + 1e-9) + 1);
|
|
70
|
+
}
|
|
71
|
+
if (dims[0] > DENSE_MAX_AXIS || dims[2] > DENSE_MAX_AXIS || dims[1] > DENSE_MAX_HEIGHT) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`bakeDenseField: ${dims.join(' × ')} samples, and a volume holds ${DENSE_MAX_AXIS} across, ` +
|
|
74
|
+
`${DENSE_MAX_HEIGHT} up and ${DENSE_MAX_AXIS} deep; a coarser spacing fits it.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Each light listed in every bucket its reach touches, in the order the scene listed them. */
|
|
79
|
+
const buckets = [
|
|
80
|
+
Math.ceil(dims[0] / BUCKET),
|
|
81
|
+
Math.ceil(dims[1] / BUCKET),
|
|
82
|
+
Math.ceil(dims[2] / BUCKET),
|
|
83
|
+
] as const;
|
|
84
|
+
const bucketCount = buckets[0] * buckets[1] * buckets[2];
|
|
85
|
+
const span = spacing * BUCKET;
|
|
86
|
+
const range = (light: FieldLight, axis: number, out: Int32Array): boolean => {
|
|
87
|
+
const at = axis === 0 ? light.x : axis === 1 ? light.y : light.z;
|
|
88
|
+
const lo = Math.floor((at - light.radius - (origin[axis] as number)) / span);
|
|
89
|
+
const hi = Math.floor((at + light.radius - (origin[axis] as number)) / span);
|
|
90
|
+
out[axis * 2] = Math.max(0, lo);
|
|
91
|
+
out[axis * 2 + 1] = Math.min((buckets[axis] as number) - 1, hi);
|
|
92
|
+
return (out[axis * 2] as number) <= (out[axis * 2 + 1] as number);
|
|
93
|
+
};
|
|
94
|
+
const cells = new Int32Array(6);
|
|
95
|
+
const start = new Uint32Array(bucketCount + 1);
|
|
96
|
+
const each = (fill: (bucket: number, light: number) => void): void => {
|
|
97
|
+
for (let index = 0; index < lights.length; index++) {
|
|
98
|
+
const light = lights[index] as FieldLight;
|
|
99
|
+
if (!(light.radius > 0)) continue;
|
|
100
|
+
if (!range(light, 0, cells) || !range(light, 1, cells) || !range(light, 2, cells)) continue;
|
|
101
|
+
for (let z = cells[4] as number; z <= (cells[5] as number); z++) {
|
|
102
|
+
for (let y = cells[2] as number; y <= (cells[3] as number); y++) {
|
|
103
|
+
for (let x = cells[0] as number; x <= (cells[1] as number); x++) {
|
|
104
|
+
fill(x + buckets[0] * (y + buckets[1] * z), index);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
each((bucket) => {
|
|
111
|
+
start[bucket + 1] = (start[bucket + 1] as number) + 1;
|
|
112
|
+
});
|
|
113
|
+
for (let b = 0; b < bucketCount; b++)
|
|
114
|
+
start[b + 1] = (start[b + 1] as number) + (start[b] as number);
|
|
115
|
+
const listed = new Uint32Array(start[bucketCount] as number);
|
|
116
|
+
const cursor = start.slice(0, bucketCount);
|
|
117
|
+
each((bucket, light) => {
|
|
118
|
+
listed[cursor[bucket] as number] = light;
|
|
119
|
+
cursor[bucket] = (cursor[bucket] as number) + 1;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const count = dims[0] * dims[1] * dims[2];
|
|
123
|
+
const light = new Float32Array(count * 4);
|
|
124
|
+
const direction = new Float32Array(count * 4);
|
|
125
|
+
for (let k = 0; k < dims[2]; k++) {
|
|
126
|
+
for (let j = 0; j < dims[1]; j++) {
|
|
127
|
+
for (let i = 0; i < dims[0]; i++) {
|
|
128
|
+
const bucket =
|
|
129
|
+
Math.floor(i / BUCKET) +
|
|
130
|
+
buckets[0] * (Math.floor(j / BUCKET) + buckets[1] * Math.floor(k / BUCKET));
|
|
131
|
+
sampleFieldLight(
|
|
132
|
+
origin[0] + i * spacing,
|
|
133
|
+
origin[1] + j * spacing,
|
|
134
|
+
origin[2] + k * spacing,
|
|
135
|
+
lights,
|
|
136
|
+
listed,
|
|
137
|
+
start[bucket] as number,
|
|
138
|
+
start[bucket + 1] as number,
|
|
139
|
+
falloff,
|
|
140
|
+
distance,
|
|
141
|
+
light,
|
|
142
|
+
direction,
|
|
143
|
+
(i + dims[0] * (j + dims[1] * k)) * 4,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { origin, spacing, dims, light, direction };
|
|
149
|
+
}
|