@driftengine/core 4.8.6 → 4.8.7
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 +71 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/render/anisotropicLobe.d.ts +10 -0
- package/dist/render/anisotropicLobe.js +19 -0
- package/dist/render/areaLights.d.ts +35 -0
- package/dist/render/areaLights.js +35 -0
- package/dist/render/backend/api.d.ts +33 -3
- package/dist/render/backend/webgl2/renderer.d.ts +50 -14
- package/dist/render/backend/webgl2/renderer.js +141 -30
- package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
- package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
- package/dist/render/backend/webgpu/particlePass.js +2 -1
- package/dist/render/backend/webgpu/renderer.d.ts +35 -1
- package/dist/render/backend/webgpu/renderer.js +155 -16
- package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
- package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/clusteredLights.d.ts +23 -5
- package/dist/render/clusteredLights.js +35 -8
- package/dist/render/driftLight/bake.d.ts +2 -0
- package/dist/render/driftLight/bake.js +1 -1
- package/dist/render/driftLight/falloff.d.ts +4 -2
- package/dist/render/driftLight/falloff.js +8 -2
- package/dist/render/gi/bounceLights.d.ts +3 -2
- package/dist/render/gi/bounceLights.js +8 -3
- package/dist/render/instanceCull.js +5 -0
- package/dist/render/instances.d.ts +19 -5
- package/dist/render/instances.js +9 -3
- package/dist/render/lightBudget.d.ts +12 -1
- package/dist/render/lightBudget.js +23 -3
- package/dist/render/lightmap.d.ts +23 -9
- package/dist/render/lightmap.js +40 -18
- package/dist/render/particlePool.d.ts +21 -0
- package/dist/render/particlePool.js +7 -0
- package/dist/render/pointLightSelection.d.ts +13 -0
- package/dist/render/pointLightSelection.js +3 -1
- package/dist/render/renderQuality.d.ts +8 -7
- package/dist/render/rendererHarness.js +3 -0
- package/dist/render/rgb9e5.d.ts +22 -0
- package/dist/render/rgb9e5.js +47 -0
- package/dist/render/sceneCapture.d.ts +29 -0
- package/dist/render/sceneCapture.js +36 -0
- package/dist/render/screenSpaceReflection.d.ts +27 -1
- package/dist/render/screenSpaceReflection.js +9 -0
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/bolt.js +7 -1
- package/dist/render/shaders/depth.js +11 -0
- package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
- package/dist/render/shaders/flat/anisotropic.js +37 -3
- package/dist/render/shaders/flat/areaLight.d.ts +1 -1
- package/dist/render/shaders/flat/areaLight.js +46 -0
- package/dist/render/shaders/flat/glassTint.d.ts +3 -2
- package/dist/render/shaders/flat/glassTint.js +3 -1
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +16 -2
- package/dist/render/shaders/flat/lobes.d.ts +6 -0
- package/dist/render/shaders/flat/lobes.js +49 -43
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +57 -22
- package/dist/render/shaders/flat/models.d.ts +5 -2
- package/dist/render/shaders/flat/models.js +100 -16
- package/dist/render/shaders/flat/preamble.js +10 -3
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
- package/dist/render/shaders/generated/depth.wgsl.js +4 -4
- package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -0
- package/dist/render/shaders/generated/flat.wgsl.js +54 -38
- package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
- package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
- package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
- package/dist/render/shaders/generated/water.wgsl.js +23 -16
- package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
- package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +13 -2
- package/dist/render/shaders/ssrTrace.js +20 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +12 -3
- package/dist/render/skinScatter.d.ts +3 -1
- package/dist/render/skinScatter.js +3 -1
- package/dist/render/ssrPass.js +1 -0
- package/dist/render/surfaceModel.d.ts +29 -8
- package/dist/render/surfaceModel.js +33 -9
- package/dist/render/surfaceTexture.d.ts +25 -1
- package/dist/render/surfaceTexture.js +27 -5
- package/package.json +6 -6
- package/src/index.ts +6 -2
- package/src/render/anisotropicLobe.ts +29 -0
- package/src/render/areaLights.ts +58 -0
- package/src/render/backend/api.ts +49 -2
- package/src/render/backend/webgl2/renderer.ts +192 -38
- package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
- package/src/render/backend/webgpu/particlePass.ts +2 -1
- package/src/render/backend/webgpu/renderer.ts +194 -19
- package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
- package/src/render/clusteredLights.ts +41 -8
- package/src/render/driftLight/bake.ts +3 -1
- package/src/render/driftLight/falloff.ts +13 -2
- package/src/render/gi/bounceLights.ts +12 -3
- package/src/render/instanceCull.ts +4 -0
- package/src/render/instances.ts +26 -6
- package/src/render/lightBudget.ts +40 -2
- package/src/render/lightmap.ts +64 -23
- package/src/render/particlePool.ts +23 -0
- package/src/render/pointLightSelection.ts +20 -1
- package/src/render/renderQuality.ts +8 -7
- package/src/render/rendererHarness.ts +3 -0
- package/src/render/rgb9e5.ts +52 -0
- package/src/render/sceneCapture.ts +56 -0
- package/src/render/screenSpaceReflection.ts +33 -1
- package/src/render/shaders/bolt.ts +7 -1
- package/src/render/shaders/depth.ts +11 -0
- package/src/render/shaders/flat/anisotropic.ts +37 -3
- package/src/render/shaders/flat/areaLight.ts +46 -0
- package/src/render/shaders/flat/glassTint.ts +9 -2
- package/src/render/shaders/flat/index.ts +21 -2
- package/src/render/shaders/flat/lobes.ts +50 -43
- package/src/render/shaders/flat/main.ts +57 -22
- package/src/render/shaders/flat/models.ts +100 -16
- package/src/render/shaders/flat/preamble.ts +10 -3
- package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +4 -4
- package/src/render/shaders/generated/flat.wgsl.ts +54 -38
- package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
- package/src/render/shaders/generated/water.wgsl.ts +23 -16
- package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
- package/src/render/shaders/recon/motion.wgsl.ts +13 -2
- package/src/render/shaders/ssrTrace.ts +20 -1
- package/src/render/shaders/water.ts +12 -3
- package/src/render/skinScatter.ts +3 -1
- package/src/render/ssrPass.ts +1 -0
- package/src/render/surfaceModel.ts +47 -10
- package/src/render/surfaceTexture.ts +69 -9
|
@@ -44,7 +44,7 @@ import { COOKIE_TILE } from '../../lightBudget.js';
|
|
|
44
44
|
import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.js';
|
|
45
45
|
import { toHalfFloats } from '../../halfFloat.js';
|
|
46
46
|
import { DEFAULT_SOURCE_RADIUS, FACE_COUNT, createResolvedPointShadows, } from '../../pointShadowImage.js';
|
|
47
|
-
import { MAX_AREA_LIGHTS } from '../../areaLights.js';
|
|
47
|
+
import { MAX_AREA_LIGHTS, packAreaSizesAndDoors } from '../../areaLights.js';
|
|
48
48
|
import { areaShadowLayerCount, castingRange, createResolvedAreaShadows, firstAreaShadowLayer, AreaShadowSet, } from '../../areaShadowSet.js';
|
|
49
49
|
import { createBakeScratch, runPointShadowBakes, selectCastingLights, } from '../../pointShadowBudget.js';
|
|
50
50
|
import { POINT_SHADOW_SAMPLER, PointShadowSystem, } from '../../pointShadowSystem.js';
|
|
@@ -54,9 +54,11 @@ import { createPanelBindGroup, createPanelBindGroupLayout, createPanelCorners, p
|
|
|
54
54
|
import { createGpuTextCube, createTextBindGroup, createTextBindGroupLayout, GpuText, textPipeline, TEXT_FRAG_FIELDS, TEXT_FRAG_SIZE, TEXT_VERT_FIELDS, TEXT_VERT_SIZE, } from './textPass.js';
|
|
55
55
|
import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.js';
|
|
56
56
|
import { GpuSurfaceTexture } from './surfaceTexturePass.js';
|
|
57
|
+
import { GpuSceneCaptureTarget } from './sceneCaptureTarget.js';
|
|
58
|
+
import { sceneCaptureTexels } from '../../sceneCapture.js';
|
|
57
59
|
import { gpuCompressedFormats } from './compressedUpload.js';
|
|
58
60
|
import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
|
|
59
|
-
import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.js';
|
|
61
|
+
import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
|
|
60
62
|
import { validateClothBinding } from '../../clothBindingData.js';
|
|
61
63
|
import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
|
|
62
64
|
import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
|
|
@@ -2227,6 +2229,97 @@ export class WebGPURenderer {
|
|
|
2227
2229
|
mipmap: false,
|
|
2228
2230
|
});
|
|
2229
2231
|
}
|
|
2232
|
+
/**
|
|
2233
|
+
* A texture the scene can be drawn into, for a material to show. See `RendererApi` and
|
|
2234
|
+
* `sceneCapture.ts`. At the world's own format, which under `hdrScene` is half floats.
|
|
2235
|
+
*/
|
|
2236
|
+
createSceneCapture(width, height) {
|
|
2237
|
+
const texture = new GpuSurfaceTexture(this.surface.device, this.pipelines, sceneCaptureTexels(width, height, this.quality.hdrScene), { wrap: 'clamp', mipmap: false });
|
|
2238
|
+
this.captureTargets.set(texture, new GpuSceneCaptureTarget(this.surface.device, width, height));
|
|
2239
|
+
return texture;
|
|
2240
|
+
}
|
|
2241
|
+
/** Each capture's attachments, beside the texture a material binds. */
|
|
2242
|
+
captureTargets = new WeakMap();
|
|
2243
|
+
/**
|
|
2244
|
+
* Whether a scene capture is being drawn: an off-screen pass as a probe bake is, so the frame's
|
|
2245
|
+
* camera, jitter, motion, refraction copy, skin's spread and late routing are left alone, but
|
|
2246
|
+
* lit by the probes and DriftLight as the frame is. See `captureScene`.
|
|
2247
|
+
*/
|
|
2248
|
+
capturePassActive = false;
|
|
2249
|
+
/**
|
|
2250
|
+
* Draw the scene into a capture from `camera`. See `RendererApi.captureScene`.
|
|
2251
|
+
*
|
|
2252
|
+
* **Its own encoder, as a probe bake's**: the frame's pass is set aside and restored, the rings
|
|
2253
|
+
* are marked and rewound around the submit, and the frame graph is flushed at both boundaries so
|
|
2254
|
+
* no recorded draw crosses one. **Radiance**: the output transform and exposure are held off.
|
|
2255
|
+
*/
|
|
2256
|
+
captureScene(capture, camera, clearColor, draw) {
|
|
2257
|
+
if (this.surface.lost)
|
|
2258
|
+
return false;
|
|
2259
|
+
const target = this.captureTargets.get(capture);
|
|
2260
|
+
const resolve = capture.layerView();
|
|
2261
|
+
if (target === undefined || resolve === null)
|
|
2262
|
+
return false;
|
|
2263
|
+
camera.updateMatrices(target.width / target.height);
|
|
2264
|
+
const { device } = this.surface;
|
|
2265
|
+
const { depth, colorMsaa } = target.attachments(this.samples, this.pipelines.format);
|
|
2266
|
+
const savedPass = this.pass;
|
|
2267
|
+
if (this.quality.frameGraph)
|
|
2268
|
+
this.flushGraph();
|
|
2269
|
+
const savedEncoder = this.encoder;
|
|
2270
|
+
this.markRings();
|
|
2271
|
+
const skinMark = this.skinPalettes.mark();
|
|
2272
|
+
const frameEpoch = this.encoderEpoch;
|
|
2273
|
+
this.encoderEpoch = ++this.encoderEpochs;
|
|
2274
|
+
const encoder = device.createCommandEncoder({ label: 'capture' });
|
|
2275
|
+
this.encoder = encoder;
|
|
2276
|
+
this.capturePassActive = true;
|
|
2277
|
+
const heldGrade = this.perFrameInts[this.materialField('uOutputTransform')] ?? 0;
|
|
2278
|
+
const heldExposure = this.perFrameFloats[this.materialField('uOutputExposure')] ?? 1;
|
|
2279
|
+
this.perFrameInts[this.materialField('uOutputTransform')] = 0;
|
|
2280
|
+
this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
|
|
2281
|
+
this.materials.dirty();
|
|
2282
|
+
const pass = encoder.beginRenderPass({
|
|
2283
|
+
label: 'capture',
|
|
2284
|
+
colorAttachments: [
|
|
2285
|
+
{
|
|
2286
|
+
view: colorMsaa ?? resolve,
|
|
2287
|
+
resolveTarget: colorMsaa === null ? undefined : resolve,
|
|
2288
|
+
loadOp: 'clear',
|
|
2289
|
+
storeOp: resolvedStoreOp(colorMsaa !== null, true, this.quality.discardResolvedAttachments),
|
|
2290
|
+
clearValue: { r: clearColor[0], g: clearColor[1], b: clearColor[2], a: 1 },
|
|
2291
|
+
},
|
|
2292
|
+
],
|
|
2293
|
+
depthStencilAttachment: {
|
|
2294
|
+
view: depth,
|
|
2295
|
+
depthClearValue: DEPTH_CLEAR,
|
|
2296
|
+
depthLoadOp: 'clear',
|
|
2297
|
+
depthStoreOp: 'discard',
|
|
2298
|
+
},
|
|
2299
|
+
});
|
|
2300
|
+
this.pass = pass;
|
|
2301
|
+
try {
|
|
2302
|
+
draw(camera);
|
|
2303
|
+
/* The callback's draws replayed into this pass before it ends: see `bakeProbe`. */
|
|
2304
|
+
if (this.quality.frameGraph)
|
|
2305
|
+
this.flushGraph();
|
|
2306
|
+
}
|
|
2307
|
+
finally {
|
|
2308
|
+
pass.end();
|
|
2309
|
+
this.pass = null;
|
|
2310
|
+
this.flushRings();
|
|
2311
|
+
device.queue.submit([encoder.finish()]);
|
|
2312
|
+
this.rewindRings(skinMark);
|
|
2313
|
+
this.encoderEpoch = frameEpoch;
|
|
2314
|
+
this.encoder = savedEncoder;
|
|
2315
|
+
this.pass = savedPass;
|
|
2316
|
+
this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
|
|
2317
|
+
this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
|
|
2318
|
+
this.materials.dirty();
|
|
2319
|
+
this.capturePassActive = false;
|
|
2320
|
+
}
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2230
2323
|
/**
|
|
2231
2324
|
* GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
|
|
2232
2325
|
*
|
|
@@ -2280,6 +2373,8 @@ export class WebGPURenderer {
|
|
|
2280
2373
|
this.modelMap = null;
|
|
2281
2374
|
this.forgetBindingsOf(texture);
|
|
2282
2375
|
texture.dispose();
|
|
2376
|
+
this.captureTargets.get(texture)?.dispose();
|
|
2377
|
+
this.captureTargets.delete(texture);
|
|
2283
2378
|
}
|
|
2284
2379
|
/**
|
|
2285
2380
|
* Drop every bind group that may hold `texture`'s view, and rebuild the ones bound now.
|
|
@@ -2458,7 +2553,7 @@ export class WebGPURenderer {
|
|
|
2458
2553
|
const model = material?.model;
|
|
2459
2554
|
warnUnpagedLightmap(model, modelMap);
|
|
2460
2555
|
if (model?.kind === 'skin' && this.skinScatter !== null) {
|
|
2461
|
-
this.skinScatter.setProfile(model.profile, model
|
|
2556
|
+
this.skinScatter.setProfile(model.profile, skinProfileDistance(model, 0), skinProfileDistance(model, 1), skinProfileDistance(model, 2));
|
|
2462
2557
|
}
|
|
2463
2558
|
this.material((f, i) => {
|
|
2464
2559
|
/* The material's four switches, one row: albedo, ORM map, emissive map, two-sided. */
|
|
@@ -2503,7 +2598,10 @@ export class WebGPURenderer {
|
|
|
2503
2598
|
f[emissive + 1] = asked?.[1] ?? 1;
|
|
2504
2599
|
f[emissive + 2] = asked?.[2] ?? 1;
|
|
2505
2600
|
/* The model's numbers and whether its map is bound. See `packModel`. */
|
|
2506
|
-
packModel(material?.model ?? null, modelMap !== null, this.modelParams);
|
|
2601
|
+
packModel(material?.model ?? null, modelMap !== null, this.modelParams, material?.physicalSpecular === true);
|
|
2602
|
+
/* The first physical highlight asked for is when the lit pipelines start carrying one. */
|
|
2603
|
+
if (material?.physicalSpecular === true)
|
|
2604
|
+
this.enableLit('PHYSICAL_SPECULAR');
|
|
2507
2605
|
f.set(this.modelParams, this.materialField('uModelParams'));
|
|
2508
2606
|
});
|
|
2509
2607
|
/*
|
|
@@ -3321,6 +3419,30 @@ export class WebGPURenderer {
|
|
|
3321
3419
|
return;
|
|
3322
3420
|
const device = this.surface.device;
|
|
3323
3421
|
device.queue.writeBuffer(batch.instances, 0, batch.staging, 0, live * PARTICLE_INSTANCE_FLOATS);
|
|
3422
|
+
this.drawParticleInstances(batch, batch.instances, live, camera, env, timeSeconds);
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Draw particles a caller's own compute wrote, where they lie. See `DeviceParticles`.
|
|
3426
|
+
*
|
|
3427
|
+
* **The pool's draw with its instance stream swapped**: the same material, blend, flipbook, soft
|
|
3428
|
+
* edge and fog, reading the caller's buffer in place of the one the pool uploads into — so nothing
|
|
3429
|
+
* crosses back to the CPU. Not sorted, and a count past what the buffer holds is cut to it rather
|
|
3430
|
+
* than handed to a draw the device would refuse with the frame.
|
|
3431
|
+
*/
|
|
3432
|
+
drawDeviceParticles(batch, particles, camera, env, timeSeconds) {
|
|
3433
|
+
const fits = Math.floor(particles.buffer.size / (PARTICLE_INSTANCE_FLOATS * 4));
|
|
3434
|
+
const live = Math.min(Math.max(0, Math.floor(particles.count)), fits);
|
|
3435
|
+
/* Not above zero covers a NaN as well: a count or a size that is no number draws nothing. */
|
|
3436
|
+
if (!this.canDraw() || !(live > 0))
|
|
3437
|
+
return;
|
|
3438
|
+
if (batch.drawnInEncoder === this.encoderEpoch)
|
|
3439
|
+
return;
|
|
3440
|
+
batch.drawnInEncoder = this.encoderEpoch;
|
|
3441
|
+
this.drawParticleInstances(batch, particles.buffer, live, camera, env, timeSeconds);
|
|
3442
|
+
}
|
|
3443
|
+
/** A pool's uniforms for this view, and its draw from `instances`: both entry points' tail. */
|
|
3444
|
+
drawParticleInstances(batch, instances, live, camera, env, timeSeconds) {
|
|
3445
|
+
const device = this.surface.device;
|
|
3324
3446
|
const v = batch.vertFloats;
|
|
3325
3447
|
const atV = (name) => (PARTICLE_VERT_FIELDS[name]?.offset ?? -4) / 4;
|
|
3326
3448
|
mat4.multiply(this.correctedViewProj, CLIP_CORRECTION, camera.viewProjection);
|
|
@@ -3392,7 +3514,7 @@ export class WebGPURenderer {
|
|
|
3392
3514
|
particleCommand.pipeline = particlePipe;
|
|
3393
3515
|
particleCommand.bindGroup = group;
|
|
3394
3516
|
particleCommand.vertexBuffers[0] = batch.corners;
|
|
3395
|
-
particleCommand.vertexBuffers[1] =
|
|
3517
|
+
particleCommand.vertexBuffers[1] = instances;
|
|
3396
3518
|
particleCommand.vertexCount = 2;
|
|
3397
3519
|
particleCommand.indexBuffer = batch.indexBuffer;
|
|
3398
3520
|
particleCommand.indexed = true;
|
|
@@ -3406,7 +3528,7 @@ export class WebGPURenderer {
|
|
|
3406
3528
|
pass.setPipeline(particlePipe);
|
|
3407
3529
|
pass.setBindGroup(0, group);
|
|
3408
3530
|
pass.setVertexBuffer(0, batch.corners);
|
|
3409
|
-
pass.setVertexBuffer(1,
|
|
3531
|
+
pass.setVertexBuffer(1, instances);
|
|
3410
3532
|
pass.setIndexBuffer(batch.indexBuffer, 'uint32');
|
|
3411
3533
|
pass.drawIndexed(PARTICLE_INDICES, live);
|
|
3412
3534
|
}
|
|
@@ -3424,7 +3546,10 @@ export class WebGPURenderer {
|
|
|
3424
3546
|
const fields = batch.fields;
|
|
3425
3547
|
const atF = (name) => (fields[name]?.offset ?? -4) / 4;
|
|
3426
3548
|
let depth = null;
|
|
3427
|
-
if (batch.softDepth > 0 &&
|
|
3549
|
+
if (batch.softDepth > 0 &&
|
|
3550
|
+
!this.reflectionPassActive &&
|
|
3551
|
+
!this.probePassActive &&
|
|
3552
|
+
!this.capturePassActive) {
|
|
3428
3553
|
if (this.openPass() !== null && this.takeVolumeDepth())
|
|
3429
3554
|
depth = this.resolvedDepthView;
|
|
3430
3555
|
if (depth === null && !this.warnedSpriteDepth) {
|
|
@@ -3717,6 +3842,7 @@ export class WebGPURenderer {
|
|
|
3717
3842
|
scatterInto(f, WATER_FRAG_FIELDS['uLightRadius'], lights.radii, 1);
|
|
3718
3843
|
scatterInto(f, WATER_FRAG_FIELDS['uLightSourceRadius'], lights.sourceRadii, 1);
|
|
3719
3844
|
scatterInto(f, WATER_FRAG_FIELDS['uLightWeight'], lights.weights, 1);
|
|
3845
|
+
scatterInto(f, WATER_FRAG_FIELDS['uLightCone'], lights.conesAndFalloffs, 4);
|
|
3720
3846
|
const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
|
|
3721
3847
|
f.set(medium.fogColor, atF('uFogColor'));
|
|
3722
3848
|
f[atF('uFogDensity')] = medium.fogDensity;
|
|
@@ -4509,8 +4635,11 @@ export class WebGPURenderer {
|
|
|
4509
4635
|
cookies: new Float32Array(0),
|
|
4510
4636
|
heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
|
|
4511
4637
|
axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
|
|
4638
|
+
conesAndFalloffs: new Float32Array(MAX_POINT_LIGHTS * 4),
|
|
4512
4639
|
heldWeights: new Float32Array(MAX_POINT_LIGHTS),
|
|
4513
4640
|
};
|
|
4641
|
+
/** The fixed arm's rectangles as `uAreaLightSize` holds them. See `packAreaSizesAndDoors`. */
|
|
4642
|
+
areaSizesAndDoors = new Float32Array(MAX_AREA_LIGHTS * 4);
|
|
4514
4643
|
/** Refilled per frame rather than allocated; see `resolveAtmosphere`. */
|
|
4515
4644
|
medium = {
|
|
4516
4645
|
fogColor: new Float32Array(3),
|
|
@@ -6566,7 +6695,7 @@ export class WebGPURenderer {
|
|
|
6566
6695
|
* the viewer sees, and bounding it through a mirrored camera puts it in the wrong half of the
|
|
6567
6696
|
* screen.
|
|
6568
6697
|
*/
|
|
6569
|
-
if (!this.reflectionPassActive) {
|
|
6698
|
+
if (!this.reflectionPassActive && !this.capturePassActive) {
|
|
6570
6699
|
mat4.copy(this.frameRawViewProj, camera.viewProjection);
|
|
6571
6700
|
/* The eye in world space, which the resolve's normals are turned to face. */
|
|
6572
6701
|
this.reconEye.set(camera.position);
|
|
@@ -6600,7 +6729,8 @@ export class WebGPURenderer {
|
|
|
6600
6729
|
* second temporal frame stepped the sequence twice a frame, so the world saw every other
|
|
6601
6730
|
* position and the anti-flicker's period never started.
|
|
6602
6731
|
*/
|
|
6603
|
-
|
|
6732
|
+
/* A capture has no resolve to undo a jitter and is no frame of the sequence. */
|
|
6733
|
+
if (this.framePresented || this.capturePassActive)
|
|
6604
6734
|
this.viewProj = this.correctedViewProj;
|
|
6605
6735
|
else
|
|
6606
6736
|
this.jitterFrame();
|
|
@@ -6928,7 +7058,8 @@ export class WebGPURenderer {
|
|
|
6928
7058
|
* uniform added on one backend is bound on both in the same change.
|
|
6929
7059
|
*/
|
|
6930
7060
|
scatterInto(f, this.fragment.fields['uLightDir'], lights.directions, 3);
|
|
6931
|
-
|
|
7061
|
+
/* The cone and, in z, the light's own falloff exponent. See `conesAndFalloffs`. */
|
|
7062
|
+
scatterInto(f, this.fragment.fields['uLightCone'], lights.conesAndFalloffs, 4);
|
|
6932
7063
|
scatterInto(f, this.fragment.fields['uLightIesProfile'], lights.iesProfiles, 1);
|
|
6933
7064
|
/* The azimuth and, in w, the light's channels. See `axesAndChannels`. */
|
|
6934
7065
|
scatterInto(f, this.fragment.fields['uLightIesAxis'], lights.axesAndChannels, 4);
|
|
@@ -6949,7 +7080,9 @@ export class WebGPURenderer {
|
|
|
6949
7080
|
scatterInto(f, this.fragment.fields['uAreaLightColor'], area.colors, 3);
|
|
6950
7081
|
scatterInto(f, this.fragment.fields['uAreaLightRight'], area.right, 3);
|
|
6951
7082
|
scatterInto(f, this.fragment.fields['uAreaLightUp'], area.up, 3);
|
|
6952
|
-
|
|
7083
|
+
/* The half extents and, in zw, the barn doors. See `packAreaSizesAndDoors`. */
|
|
7084
|
+
packAreaSizesAndDoors(area, this.areaSizesAndDoors);
|
|
7085
|
+
scatterInto(f, this.fragment.fields['uAreaLightSize'], this.areaSizesAndDoors, 4);
|
|
6953
7086
|
scatterInto(f, this.fragment.fields['uAreaLightTwoSided'], area.twoSided, 1);
|
|
6954
7087
|
}
|
|
6955
7088
|
/*
|
|
@@ -9631,7 +9764,7 @@ export class WebGPURenderer {
|
|
|
9631
9764
|
drawsLate() {
|
|
9632
9765
|
return (this.latePass !== null &&
|
|
9633
9766
|
this.oitEncoder === null &&
|
|
9634
|
-
routesLate(this.reconstructing, this.reflectionPassActive, this.probePassActive, this.framePresented));
|
|
9767
|
+
routesLate(this.reconstructing, this.reflectionPassActive, this.probePassActive || this.capturePassActive, this.framePresented));
|
|
9635
9768
|
}
|
|
9636
9769
|
/**
|
|
9637
9770
|
* The command a blended draw records into: the late pool while reconstructing, the frame graph
|
|
@@ -9693,7 +9826,7 @@ export class WebGPURenderer {
|
|
|
9693
9826
|
}
|
|
9694
9827
|
/** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
|
|
9695
9828
|
tracksMotionNow() {
|
|
9696
|
-
return tracksMotion(this.reconstructing, this.reflectionPassActive, this.probePassActive, this.framePresented);
|
|
9829
|
+
return tracksMotion(this.reconstructing, this.reflectionPassActive, this.probePassActive || this.capturePassActive, this.framePresented);
|
|
9697
9830
|
}
|
|
9698
9831
|
/**
|
|
9699
9832
|
* What this draw's motion is, recorded for the motion pass.
|
|
@@ -10094,6 +10227,8 @@ export class WebGPURenderer {
|
|
|
10094
10227
|
f.set(surface.axis, base + at('uSsrAxis'));
|
|
10095
10228
|
f.set(surface.tint, base + at('uSsrTint'));
|
|
10096
10229
|
f[base + at('uSsrStrength')] = surface.strength;
|
|
10230
|
+
f[base + at('uSsrFresnel')] = surface.fresnel ? 1 : 0;
|
|
10231
|
+
f[base + at('uSsrFresnel') + 1] = surface.roughness;
|
|
10097
10232
|
f[base + at('uSsrFacingCos')] = surface.facingCos;
|
|
10098
10233
|
f[base + at('uSsrReach')] = surface.reachM;
|
|
10099
10234
|
f[base + at('uSsrThickness')] = surface.thicknessM;
|
|
@@ -10654,7 +10789,7 @@ export class WebGPURenderer {
|
|
|
10654
10789
|
* glass, and a frame that refracts nothing breaks no pass at all.
|
|
10655
10790
|
*/
|
|
10656
10791
|
takeRefractSnapshot() {
|
|
10657
|
-
if (this.reflectionPassActive || this.probePassActive)
|
|
10792
|
+
if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
|
|
10658
10793
|
return false;
|
|
10659
10794
|
if (this.refractSnapshotTaken)
|
|
10660
10795
|
return true;
|
|
@@ -10737,6 +10872,7 @@ export class WebGPURenderer {
|
|
|
10737
10872
|
!this.skinScattered &&
|
|
10738
10873
|
!this.reflectionPassActive &&
|
|
10739
10874
|
!this.probePassActive &&
|
|
10875
|
+
!this.capturePassActive &&
|
|
10740
10876
|
!this.framePresented &&
|
|
10741
10877
|
!this.overlayActive &&
|
|
10742
10878
|
this.oitMode === 'none');
|
|
@@ -10794,6 +10930,8 @@ export class WebGPURenderer {
|
|
|
10794
10930
|
return;
|
|
10795
10931
|
if (this.reflectionPassActive || this.probePassActive || this.oitReplaying)
|
|
10796
10932
|
return;
|
|
10933
|
+
if (this.capturePassActive)
|
|
10934
|
+
return;
|
|
10797
10935
|
const encoder = this.encoder;
|
|
10798
10936
|
const depth = this.depthView;
|
|
10799
10937
|
const resolved = this.resolvedDepthView;
|
|
@@ -10851,7 +10989,7 @@ export class WebGPURenderer {
|
|
|
10851
10989
|
* own terms: `resolvedDepthView` holds the *frame's* depth, and clamping a mirrored beam
|
|
10852
10990
|
* against it would stop it at geometry that is not in front of it.
|
|
10853
10991
|
*/
|
|
10854
|
-
if (this.reflectionPassActive)
|
|
10992
|
+
if (this.reflectionPassActive || this.capturePassActive)
|
|
10855
10993
|
return false;
|
|
10856
10994
|
if (this.resolvedDepthView === null)
|
|
10857
10995
|
return false;
|
|
@@ -11408,8 +11546,9 @@ export class WebGPURenderer {
|
|
|
11408
11546
|
* any future branch added to one belongs in the other.
|
|
11409
11547
|
*/
|
|
11410
11548
|
targetPipelines() {
|
|
11411
|
-
if (this.probePassActive || this.reflectionPassActive)
|
|
11549
|
+
if (this.probePassActive || this.reflectionPassActive || this.capturePassActive) {
|
|
11412
11550
|
return this.pipelines;
|
|
11551
|
+
}
|
|
11413
11552
|
return this.overlayActive || this.framePresented ? this.overlayPipelines : this.pipelines;
|
|
11414
11553
|
}
|
|
11415
11554
|
/** End and submit a reopened pass. Safe to call when there is none. */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scene capture's attachments on WebGPU: the depth and, above one sample, the multisampled colour
|
|
3
|
+
* its pass draws into and resolves from — the capture's own texture being the resolve, since **a
|
|
4
|
+
* texture cannot be both multisampled and sampled**. At the world's sample count and format, because
|
|
5
|
+
* a capture draws the world with the world's pipelines: a pipeline whose count or format disagrees
|
|
6
|
+
* with its attachment is rejected at `finish` and takes the whole capture with it. Remade only if
|
|
7
|
+
* either has changed since. See `sceneCapture.ts` for what a capture is.
|
|
8
|
+
*/
|
|
9
|
+
export declare class GpuSceneCaptureTarget {
|
|
10
|
+
private readonly device;
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
private depth;
|
|
14
|
+
private depthView;
|
|
15
|
+
private colorMsaa;
|
|
16
|
+
private colorMsaaView;
|
|
17
|
+
private key;
|
|
18
|
+
constructor(device: GPUDevice, width: number, height: number);
|
|
19
|
+
attachments(samples: number, format: GPUTextureFormat): {
|
|
20
|
+
depth: GPUTextureView;
|
|
21
|
+
colorMsaa: GPUTextureView | null;
|
|
22
|
+
};
|
|
23
|
+
dispose(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DEPTH_FORMAT } from '../../depthConvention.js';
|
|
2
|
+
/**
|
|
3
|
+
* A scene capture's attachments on WebGPU: the depth and, above one sample, the multisampled colour
|
|
4
|
+
* its pass draws into and resolves from — the capture's own texture being the resolve, since **a
|
|
5
|
+
* texture cannot be both multisampled and sampled**. At the world's sample count and format, because
|
|
6
|
+
* a capture draws the world with the world's pipelines: a pipeline whose count or format disagrees
|
|
7
|
+
* with its attachment is rejected at `finish` and takes the whole capture with it. Remade only if
|
|
8
|
+
* either has changed since. See `sceneCapture.ts` for what a capture is.
|
|
9
|
+
*/
|
|
10
|
+
export class GpuSceneCaptureTarget {
|
|
11
|
+
device;
|
|
12
|
+
width;
|
|
13
|
+
height;
|
|
14
|
+
depth = null;
|
|
15
|
+
depthView = null;
|
|
16
|
+
colorMsaa = null;
|
|
17
|
+
colorMsaaView = null;
|
|
18
|
+
key = '';
|
|
19
|
+
constructor(device, width, height) {
|
|
20
|
+
this.device = device;
|
|
21
|
+
this.width = width;
|
|
22
|
+
this.height = height;
|
|
23
|
+
}
|
|
24
|
+
attachments(samples, format) {
|
|
25
|
+
const key = `${samples}:${format}`;
|
|
26
|
+
if (this.depthView === null || key !== this.key) {
|
|
27
|
+
this.dispose();
|
|
28
|
+
this.key = key;
|
|
29
|
+
this.depth = this.device.createTexture({
|
|
30
|
+
label: 'capture.depth',
|
|
31
|
+
size: [this.width, this.height],
|
|
32
|
+
format: DEPTH_FORMAT,
|
|
33
|
+
sampleCount: samples,
|
|
34
|
+
usage: 0x10, // RENDER_ATTACHMENT
|
|
35
|
+
});
|
|
36
|
+
this.depthView = this.depth.createView();
|
|
37
|
+
this.colorMsaa =
|
|
38
|
+
samples > 1
|
|
39
|
+
? this.device.createTexture({
|
|
40
|
+
label: 'capture.colorMsaa',
|
|
41
|
+
size: [this.width, this.height],
|
|
42
|
+
format,
|
|
43
|
+
sampleCount: samples,
|
|
44
|
+
usage: 0x10, // RENDER_ATTACHMENT
|
|
45
|
+
})
|
|
46
|
+
: null;
|
|
47
|
+
this.colorMsaaView = this.colorMsaa?.createView() ?? null;
|
|
48
|
+
}
|
|
49
|
+
return { depth: this.depthView, colorMsaa: this.colorMsaaView };
|
|
50
|
+
}
|
|
51
|
+
dispose() {
|
|
52
|
+
this.depth?.destroy();
|
|
53
|
+
this.colorMsaa?.destroy();
|
|
54
|
+
this.depth = null;
|
|
55
|
+
this.depthView = null;
|
|
56
|
+
this.colorMsaa = null;
|
|
57
|
+
this.colorMsaaView = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -20,4 +20,4 @@
|
|
|
20
20
|
* sort. `scripts/cluster-check.mjs` is what holds that, and the 2026-08-17 rule about two
|
|
21
21
|
* implementations of one decision is why it exists at all.
|
|
22
22
|
*/
|
|
23
|
-
export declare const CLUSTER_BIN_WGSL = "\nconst CLUSTER_X: u32 = 16u;\nconst CLUSTER_Y: u32 = 9u;\nconst CLUSTER_Z: u32 = 24u;\nconst CLUSTER_COUNT: u32 = 3456u;\nconst CLUSTER_TEXELS: u32 = 20u;\nconst MAX_LIGHTS_PER_CLUSTER: u32 = 76u;\nconst LIGHT_TEXELS: u32 =
|
|
23
|
+
export declare const CLUSTER_BIN_WGSL = "\nconst CLUSTER_X: u32 = 16u;\nconst CLUSTER_Y: u32 = 9u;\nconst CLUSTER_Z: u32 = 24u;\nconst CLUSTER_COUNT: u32 = 3456u;\nconst CLUSTER_TEXELS: u32 = 20u;\nconst MAX_LIGHTS_PER_CLUSTER: u32 = 76u;\nconst LIGHT_TEXELS: u32 = 6u;\nconst LIGHT_REGION_TEXELS: u32 = 1920u;\nconst TABLE_WIDTH: u32 = 320u;\n\nstruct Params {\n view: mat4x4<f32>,\n /** near, far, tan(fovY/2), aspect. */\n frustum: vec4<f32>,\n lightCount: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n/**\n * The records as `writeLightRecord` lays them out, as bits. **`u32`, not `f32`**, because one\n * slot holds two halves, and a zero weight read as a float would be a subnormal a GPU is free to\n * flush on the way through; read as bits, the copy into the table cannot change them.\n */\n@group(0) @binding(1) var<storage, read> lights: array<vec4<u32>>;\n/**\n * Write-only, which is all core WGSL offers and all the fixed-slot layout needs.\n *\n * A read-modify-write list would need a storage buffer and an atomic counter, and it is the reason\n * a cluster owns a fixed run rather than appending into a shared one.\n */\n@group(0) @binding(2) var table: texture_storage_2d<rgba32uint, write>;\n\n/** A flat texel index to its place in the table. TABLE_WIDTH divides both regions exactly. */\nfn texelAt(texel: u32) -> vec2<i32> {\n return vec2<i32>(i32(texel % TABLE_WIDTH), i32(texel / TABLE_WIDTH));\n}\n\nfn sliceNearDepth(slice: f32) -> f32 {\n return params.frustum.x * pow(params.frustum.y / params.frustum.x, slice / f32(CLUSTER_Z));\n}\n\n/** Squared distance from a point to an axis-aligned box, zero inside it. */\nfn distanceSqToBounds(p: vec3<f32>, lo: vec3<f32>, hi: vec3<f32>) -> f32 {\n let d = max(max(lo - p, p - hi), vec3<f32>(0.0));\n return dot(d, d);\n}\n\n/** A light's view-space centre, with depth positive along the view direction. */\nfn viewOf(light: u32) -> vec3<f32> {\n let world = bitcast<vec4<f32>>(lights[light * LIGHT_TEXELS]);\n let v = params.view * vec4<f32>(world.xyz, 1.0);\n return vec3<f32>(v.x, v.y, -v.z);\n}\n\n/**\n * Squared distance from a light's view-space centre to a point.\n *\n * **Squared, matching `centreDistanceSq` in `clusteredLights.ts` rather than taking a root.** The\n * ranking is identical either way, and a square root is one more operation for the two\n * implementations to round differently.\n */\nfn centreDistanceSq(light: u32, centre: vec3<f32>) -> f32 {\n let d = viewOf(light) - centre;\n return dot(d, d);\n}\n\n/**\n * Whether a spot's cone misses this froxel: `coneBoxGap` in `clusteredLights.ts`, the same\n * arithmetic in the same order \u2014 the froxel's bounding sphere against the infinite cone, its apex\n * included, which can keep a light the shader would not draw and never drop one it would. A light\n * with no cone carries a cosine of -2 and never reaches the test.\n */\nfn outsideCone(light: u32, apex: vec3<f32>, lo: vec3<f32>, hi: vec3<f32>) -> bool {\n let cosOuter = bitcast<f32>(lights[light * LIGHT_TEXELS + 3u].y);\n if (cosOuter <= -1.0) {\n return false;\n }\n let world = bitcast<vec3<f32>>(lights[light * LIGHT_TEXELS + 2u].yzw);\n let a = (params.view * vec4<f32>(world, 0.0)).xyz;\n let axis = vec3<f32>(a.x, a.y, -a.z);\n let len = length(axis);\n /* The shader measures the cone against the direction as given, so a long one is a wider cone. */\n if (len <= 1e-6 || cosOuter / len <= -1.0) {\n return false;\n }\n let cosA = cosOuter / len;\n let sinA = sqrt(max(0.0, 1.0 - cosA * cosA));\n let unit = axis / len;\n let extent = (hi - lo) * 0.5;\n let toCentre = (lo + hi) * 0.5 - apex;\n let along = dot(toCentre, unit);\n let lengthSq = dot(toCentre, toCentre);\n let across = sqrt(max(lengthSq - along * along, 0.0));\n let reach = sqrt(lengthSq);\n var distance = cosA * across - along * sinA;\n if (along < -sinA * reach) {\n distance = reach;\n }\n return distance - length(extent) > 0.0;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let id = gid.x;\n\n /*\n * The light records, written by the first invocations that have one.\n *\n * **Two jobs in one kernel, and it is deliberate.** The records have to reach the same texture\n * the clusters do, because the lit pass reads both through one texture unit; writing them from\n * the CPU instead would mean a partial texture upload racing a dispatch over the same resource.\n * There are always more clusters than lights, so every record has an invocation.\n */\n if (id < params.lightCount) {\n let record = id * LIGHT_TEXELS;\n for (var t: u32 = 0u; t < LIGHT_TEXELS; t = t + 1u) {\n textureStore(table, texelAt(record + t), lights[record + t]);\n }\n }\n\n if (id >= CLUSTER_COUNT) {\n return;\n }\n\n let k = id / (CLUSTER_X * CLUSTER_Y);\n let j = (id - k * CLUSTER_X * CLUSTER_Y) / CLUSTER_X;\n let i = id - k * CLUSTER_X * CLUSTER_Y - j * CLUSTER_X;\n\n /* The AABB of the froxel's eight corners, exactly as `clusterViewBounds` derives it. */\n let zNear = sliceNearDepth(f32(k));\n let zFar = sliceNearDepth(f32(k + 1u));\n let xLo = -1.0 + 2.0 * f32(i) / f32(CLUSTER_X);\n let xHi = -1.0 + 2.0 * f32(i + 1u) / f32(CLUSTER_X);\n let yLo = -1.0 + 2.0 * f32(j) / f32(CLUSTER_Y);\n let yHi = -1.0 + 2.0 * f32(j + 1u) / f32(CLUSTER_Y);\n let halfHNear = zNear * params.frustum.z;\n let halfHFar = zFar * params.frustum.z;\n let halfWNear = halfHNear * params.frustum.w;\n let halfWFar = halfHFar * params.frustum.w;\n\n let lo = vec3<f32>(\n min(min(xLo * halfWNear, xLo * halfWFar), min(xHi * halfWNear, xHi * halfWFar)),\n min(min(yLo * halfHNear, yLo * halfHFar), min(yHi * halfHNear, yHi * halfHFar)),\n zNear,\n );\n let hi = vec3<f32>(\n max(max(xLo * halfWNear, xLo * halfWFar), max(xHi * halfWNear, xHi * halfWFar)),\n max(max(yLo * halfHNear, yLo * halfHFar), max(yHi * halfHNear, yHi * halfHFar)),\n zFar,\n );\n let centre = (lo + hi) * 0.5;\n\n var indices: array<u32, MAX_LIGHTS_PER_CLUSTER>;\n for (var n: u32 = 0u; n < MAX_LIGHTS_PER_CLUSTER; n = n + 1u) {\n indices[n] = 0u;\n }\n var count: u32 = 0u;\n\n /* Increasing light index, which is what makes the list sorted without a sort. */\n for (var light: u32 = 0u; light < params.lightCount; light = light + 1u) {\n /* A rectangle carries its reach negated, which is how the lit pass tells it from a lamp. */\n let signedRadius = bitcast<f32>(lights[light * LIGHT_TEXELS].w);\n let radius = abs(signedRadius);\n let v = viewOf(light);\n if (v.z + radius <= 0.0) {\n continue;\n }\n if (distanceSqToBounds(v, lo, hi) > radius * radius) {\n continue;\n }\n if (signedRadius >= 0.0 && outsideCone(light, v, lo, hi)) {\n continue;\n }\n\n if (count < MAX_LIGHTS_PER_CLUSTER) {\n indices[count] = light;\n count = count + 1u;\n continue;\n }\n\n /*\n * Full. Drop the farthest from the cluster's centre if this one is nearer.\n *\n * **The only float comparison that decides anything here**, and the reason the *order* is an\n * integer: a float rank cannot be relied on to agree between this and the JavaScript binner,\n * so it is confined to membership, where the conformance script forgives a light sitting\n * within epsilon of a boundary.\n */\n var worstSlot: i32 = -1;\n var worst = centreDistanceSq(light, centre);\n for (var n: u32 = 0u; n < MAX_LIGHTS_PER_CLUSTER; n = n + 1u) {\n let d = centreDistanceSq(indices[n], centre);\n if (d > worst) {\n worst = d;\n worstSlot = i32(n);\n }\n }\n if (worstSlot < 0) {\n continue;\n }\n indices[u32(worstSlot)] = light;\n\n /* Insertion sort over at most 28 integers, so the bytes stay ascending after a replacement. */\n for (var n: u32 = 1u; n < MAX_LIGHTS_PER_CLUSTER; n = n + 1u) {\n let value = indices[n];\n var m: i32 = i32(n) - 1;\n while (m >= 0 && indices[u32(m)] > value) {\n indices[u32(m) + 1u] = indices[u32(m)];\n m = m - 1;\n }\n indices[u32(m + 1)] = value;\n }\n }\n\n let base = LIGHT_REGION_TEXELS + id * CLUSTER_TEXELS;\n textureStore(table, texelAt(base), vec4<u32>(count, 0u, 0u, 0u));\n for (var t: u32 = 0u; t < CLUSTER_TEXELS - 1u; t = t + 1u) {\n textureStore(\n table,\n texelAt(base + 1u + t),\n vec4<u32>(indices[t * 4u], indices[t * 4u + 1u], indices[t * 4u + 2u], indices[t * 4u + 3u]),\n );\n }\n}\n";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SurfaceTextureOptions } from '../../surfaceTexture.ts';
|
|
2
2
|
import type { LightmapTexels } from '../../lightmap.ts';
|
|
3
|
+
import type { SceneCaptureTexels } from '../../sceneCapture.ts';
|
|
3
4
|
import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
|
|
4
5
|
import type { PipelineCache } from './pipelineCache.ts';
|
|
5
6
|
/** How many levels an image of this size has, counting the one it arrives with. */
|
|
@@ -63,11 +64,13 @@ export declare class GpuSurfaceTexture {
|
|
|
63
64
|
effectsView: GPUTextureView | null;
|
|
64
65
|
/** The block format a compressed texture holds, or null for one uploaded from images. */
|
|
65
66
|
private readonly blockFormat;
|
|
66
|
-
constructor(device: GPUDevice, pipelines: PipelineCache, source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels, options?: SurfaceTextureOptions, compressed?: readonly CompressedTextureFormat[]);
|
|
67
|
+
constructor(device: GPUDevice, pipelines: PipelineCache, source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels | SceneCaptureTexels, options?: SurfaceTextureOptions, compressed?: readonly CompressedTextureFormat[]);
|
|
67
68
|
/** A view of every layer, as the shaders' `texture_2d_array` bindings read it. */
|
|
68
69
|
private arrayView;
|
|
69
70
|
/** The view every binding of this image holds. A new one when `update` changes the size. */
|
|
70
71
|
get view(): GPUTextureView;
|
|
72
|
+
/** The only layer as a plain 2D view, which a scene capture's pass resolves into. */
|
|
73
|
+
layerView(): GPUTextureView | null;
|
|
71
74
|
/**
|
|
72
75
|
* Replace the pixels, keeping the handle and its sampler.
|
|
73
76
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from '../../surfaceEffects.js';
|
|
2
2
|
import { toHalfFloats } from '../../halfFloat.js';
|
|
3
3
|
import { isLightmapTexels } from '../../lightmap.js';
|
|
4
|
+
import { isSceneCaptureTexels } from '../../sceneCapture.js';
|
|
4
5
|
import { layerSize, refuseArrayUpdate, sourceSize } from '../../textureSource.js';
|
|
5
6
|
import { compressedLayers, planBlocks, refuseBlockUpdate } from '../../compressedSource.js';
|
|
6
7
|
import { createCompressedTexture } from './compressedUpload.js';
|
|
@@ -156,24 +157,44 @@ export class GpuSurfaceTexture {
|
|
|
156
157
|
constructor(device, pipelines, source, options = {}, compressed = []) {
|
|
157
158
|
this.device = device;
|
|
158
159
|
this.pipelines = pipelines;
|
|
159
|
-
/* A
|
|
160
|
+
/* A scene capture's target: one empty layer the world is drawn into. See `sceneCapture.ts`. */
|
|
161
|
+
const capture = isSceneCaptureTexels(source) ? source : null;
|
|
162
|
+
/* A lightmap's page: two layers of bytes, uploaded as they are. See `lightmap.ts`. */
|
|
160
163
|
const baked = isLightmapTexels(source) ? source : null;
|
|
161
|
-
const listed = (baked !== null ? [] : Array.isArray(source) ? source : [source]);
|
|
164
|
+
const listed = (baked !== null || capture !== null ? [] : Array.isArray(source) ? source : [source]);
|
|
162
165
|
const blocks = compressedLayers(listed);
|
|
163
166
|
const srgb = (options.colorSpace ?? 'linear') === 'srgb';
|
|
164
167
|
/* `SRGB8_ALPHA8`'s equivalent. Decoded in the sampler, before filtering, which is the only
|
|
165
|
-
place it is correct — `surfaceTexture.ts` makes the argument in full.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
place it is correct — `surfaceTexture.ts` makes the argument in full. A capture takes the
|
|
169
|
+
world's own format, which its pass resolves into. */
|
|
170
|
+
this.format =
|
|
171
|
+
capture !== null
|
|
172
|
+
? pipelines.format
|
|
173
|
+
: srgb && baked === null
|
|
174
|
+
? 'rgba8unorm-srgb'
|
|
175
|
+
: 'rgba8unorm';
|
|
176
|
+
if (capture !== null) {
|
|
177
|
+
this.layers = 1;
|
|
178
|
+
this.blockFormat = null;
|
|
179
|
+
this.mipmapped = false;
|
|
180
|
+
this.width = capture.width;
|
|
181
|
+
this.height = capture.height;
|
|
182
|
+
this.levels = 1;
|
|
183
|
+
this.texture = this.allocate();
|
|
184
|
+
}
|
|
185
|
+
else if (baked !== null) {
|
|
168
186
|
/* No chain: a page is read at its own resolution, and a level of it would blend regions. */
|
|
169
|
-
this.layers = baked.layers;
|
|
187
|
+
this.layers = baked.layers.length;
|
|
170
188
|
this.blockFormat = null;
|
|
171
189
|
this.mipmapped = false;
|
|
172
190
|
this.width = baked.width;
|
|
173
191
|
this.height = baked.height;
|
|
174
192
|
this.levels = 1;
|
|
175
193
|
this.texture = this.allocate();
|
|
176
|
-
|
|
194
|
+
/* A layer at a time, so a page's arrays are uploaded where they lie rather than joined. */
|
|
195
|
+
for (let layer = 0; layer < this.layers; layer++) {
|
|
196
|
+
device.queue.writeTexture({ texture: this.texture, origin: [0, 0, layer] }, baked.layers[layer], { bytesPerRow: baked.width * 4, rowsPerImage: baked.height }, { width: baked.width, height: baked.height, depthOrArrayLayers: 1 });
|
|
197
|
+
}
|
|
177
198
|
}
|
|
178
199
|
else if (blocks !== null) {
|
|
179
200
|
/* Refused before anything is allocated; the stored chain, or level 0 alone where no chain
|
|
@@ -230,8 +251,9 @@ export class GpuSurfaceTexture {
|
|
|
230
251
|
? Math.max(1, Math.floor(options.anisotropy ?? DEFAULT_ANISOTROPY))
|
|
231
252
|
: 1,
|
|
232
253
|
});
|
|
233
|
-
if (blocks === null && baked === null)
|
|
254
|
+
if (blocks === null && baked === null && capture === null) {
|
|
234
255
|
this.upload(listed);
|
|
256
|
+
}
|
|
235
257
|
/* Half floats, as `renderer.ts` uploads it: filterable, so the ordinary float layout takes it,
|
|
236
258
|
and read by `textureLoad` alone, so the filter never runs. */
|
|
237
259
|
const effects = options.effects ?? [];
|
|
@@ -255,6 +277,10 @@ export class GpuSurfaceTexture {
|
|
|
255
277
|
get view() {
|
|
256
278
|
return this.current;
|
|
257
279
|
}
|
|
280
|
+
/** The only layer as a plain 2D view, which a scene capture's pass resolves into. */
|
|
281
|
+
layerView() {
|
|
282
|
+
return (this.texture?.createView({ dimension: '2d', baseArrayLayer: 0, arrayLayerCount: 1 }) ?? null);
|
|
283
|
+
}
|
|
258
284
|
/**
|
|
259
285
|
* Replace the pixels, keeping the handle and its sampler.
|
|
260
286
|
*
|
|
@@ -11,7 +11,7 @@ import type { UniformFields } from './scatterPass.ts';
|
|
|
11
11
|
export declare const WATER_VERT_FIELDS: UniformFields;
|
|
12
12
|
export declare const WATER_VERT_SIZE: 208;
|
|
13
13
|
export declare const WATER_FRAG_FIELDS: UniformFields;
|
|
14
|
-
export declare const WATER_FRAG_SIZE:
|
|
14
|
+
export declare const WATER_FRAG_SIZE: 1504;
|
|
15
15
|
/** The two meshes and the numbers the grid decided, behind the surface's opaque handle. */
|
|
16
16
|
export interface GpuWater {
|
|
17
17
|
/** The ocean: near sheet and far skirt in one buffer. */
|