@zephyr3d/scene 0.9.26 → 0.9.27
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/dist/animation/animationset.js +92 -30
- package/dist/animation/animationset.js.map +1 -1
- package/dist/animation/joint_dynamics/convex_collider.js +320 -0
- package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
- package/dist/animation/skeleton.js +21 -1
- package/dist/animation/skeleton.js.map +1 -1
- package/dist/asset/model.js +6 -2
- package/dist/asset/model.js.map +1 -1
- package/dist/camera/camera.js +75 -23
- package/dist/camera/camera.js.map +1 -1
- package/dist/index.d.ts +4764 -3600
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/material/hairstrand.js +235 -235
- package/dist/material/hairstrand.js.map +1 -1
- package/dist/material/material.js +296 -296
- package/dist/material/material.js.map +1 -1
- package/dist/material/mixins/lit.js +33 -1
- package/dist/material/mixins/lit.js.map +1 -1
- package/dist/material/shader/helper.js +404 -3
- package/dist/material/shader/helper.js.map +1 -1
- package/dist/material/water.js +2057 -72
- package/dist/material/water.js.map +1 -1
- package/dist/posteffect/motionblur.js +663 -58
- package/dist/posteffect/motionblur.js.map +1 -1
- package/dist/posteffect/posteffect.js +12 -1
- package/dist/posteffect/posteffect.js.map +1 -1
- package/dist/posteffect/sao.js +46 -44
- package/dist/posteffect/sao.js.map +1 -1
- package/dist/posteffect/sss.js +30 -2
- package/dist/posteffect/sss.js.map +1 -1
- package/dist/render/abuffer_oit.js +37 -37
- package/dist/render/abuffer_oit.js.map +1 -1
- package/dist/render/clipmap.js +156 -5
- package/dist/render/clipmap.js.map +1 -1
- package/dist/render/cull_visitor.js +1 -0
- package/dist/render/cull_visitor.js.map +1 -1
- package/dist/render/dualdepthpeeling_oit.js +49 -49
- package/dist/render/dualdepthpeeling_oit.js.map +1 -1
- package/dist/render/fbm_wavegenerator.js +3 -3
- package/dist/render/fbm_wavegenerator.js.map +1 -1
- package/dist/render/fft_wavegenerator.js +57 -15
- package/dist/render/fft_wavegenerator.js.map +1 -1
- package/dist/render/gerstner_wavegenerator.js +127 -20
- package/dist/render/gerstner_wavegenerator.js.map +1 -1
- package/dist/render/hzb.js +80 -22
- package/dist/render/hzb.js.map +1 -1
- package/dist/render/lightpass.js +6 -3
- package/dist/render/lightpass.js.map +1 -1
- package/dist/render/oit.js +11 -11
- package/dist/render/oit.js.map +1 -1
- package/dist/render/render_queue.js +36 -0
- package/dist/render/render_queue.js.map +1 -1
- package/dist/render/renderer.js +4 -0
- package/dist/render/renderer.js.map +1 -1
- package/dist/render/rendergraph/blackboard.js +1 -0
- package/dist/render/rendergraph/blackboard.js.map +1 -1
- package/dist/render/rendergraph/forward_plus_builder.js +318 -9
- package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
- package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
- package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
- package/dist/render/rendergraph/history_resources.js +8 -1
- package/dist/render/rendergraph/history_resources.js.map +1 -1
- package/dist/render/underwater.js +624 -0
- package/dist/render/underwater.js.map +1 -0
- package/dist/render/water_caustics.js +1380 -0
- package/dist/render/water_caustics.js.map +1 -0
- package/dist/render/weightedblended_oit.js +33 -33
- package/dist/render/weightedblended_oit.js.map +1 -1
- package/dist/scene/water.js +540 -4
- package/dist/scene/water.js.map +1 -1
- package/dist/shaders/water.js +22 -19
- package/dist/shaders/water.js.map +1 -1
- package/dist/shaders/water_medium.js +56 -0
- package/dist/shaders/water_medium.js.map +1 -0
- package/dist/shadow/shadow_region.js +49 -49
- package/dist/shadow/shadow_region.js.map +1 -1
- package/dist/utility/serialization/manager.js +3 -1
- package/dist/utility/serialization/manager.js.map +1 -1
- package/dist/utility/serialization/scene/animation.js +31 -2
- package/dist/utility/serialization/scene/animation.js.map +1 -1
- package/dist/utility/serialization/scene/camera.js +53 -25
- package/dist/utility/serialization/scene/camera.js.map +1 -1
- package/dist/utility/serialization/scene/light.js.map +1 -1
- package/dist/utility/serialization/scene/water.js +1101 -21
- package/dist/utility/serialization/scene/water.js.map +1 -1
- package/dist/utility/shprojector.js +87 -87
- package/dist/utility/shprojector.js.map +1 -1
- package/package.json +1 -1
- package/dist/render/rendergraph/frame_graph_context.js +0 -41
- package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
- package/dist/shaders/ssgi.js +0 -63
- package/dist/shaders/ssgi.js.map +0 -1
- package/dist/shaders/ssgi_reprojection.js +0 -118
- package/dist/shaders/ssgi_reprojection.js.map +0 -1
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Vector4, DEPTH_CLEAR_VALUE, DEPTH_COMPARE_DEFAULT } from '@zephyr3d/base';
|
|
2
2
|
import { RenderQueue } from '../render_queue.js';
|
|
3
3
|
import { LightPass } from '../lightpass.js';
|
|
4
4
|
import { ShadowMapPass } from '../shadowmap_pass.js';
|
|
5
5
|
import { DepthPass } from '../depthpass.js';
|
|
6
6
|
import { ClusteredLight } from '../cluster_light.js';
|
|
7
7
|
import { ShadowMaskRenderer } from '../shadow_mask_pass.js';
|
|
8
|
-
import { buildHiZ } from '../hzb.js';
|
|
8
|
+
import { getHiZFormat, buildHiZ } from '../hzb.js';
|
|
9
9
|
import '../primitive.js';
|
|
10
10
|
import '@zephyr3d/device';
|
|
11
11
|
import { CopyBlitter } from '../../blitter/copy.js';
|
|
12
12
|
import { fetchSampler } from '../../utility/misc.js';
|
|
13
13
|
import { MaterialVaryingFlags } from '../../values.js';
|
|
14
|
-
import { ShaderHelper } from '../../material/shader/helper.js';
|
|
14
|
+
import { ShaderHelper, MAX_CAUSTIC_WATERS } from '../../material/shader/helper.js';
|
|
15
|
+
import { REFRACT_BLUR_MAX_LOD } from '../../material/water.js';
|
|
15
16
|
import { PostEffectLayer, AbstractPostEffect } from '../../posteffect/posteffect.js';
|
|
16
17
|
import { RenderGraph } from './rendergraph.js';
|
|
17
18
|
import { RenderGraphExecutor } from './executor.js';
|
|
@@ -31,12 +32,16 @@ import '../../shapes/torus.js';
|
|
|
31
32
|
import '../../shapes/plane.js';
|
|
32
33
|
import '../../shapes/sphere.js';
|
|
33
34
|
import '../../shapes/tetrahedron.js';
|
|
35
|
+
import { WaterCausticsRenderer } from '../water_caustics.js';
|
|
36
|
+
import { UnderwaterRenderer, selectUnderwaterSource } from '../underwater.js';
|
|
34
37
|
|
|
35
38
|
const _scenePass = new LightPass();
|
|
36
39
|
const _depthPass = new DepthPass();
|
|
37
40
|
const _shadowMapPass = new ShadowMapPass();
|
|
38
41
|
const _clusters = [];
|
|
39
42
|
const _shadowMaskRenderer = new ShadowMaskRenderer();
|
|
43
|
+
const _waterCausticsRenderer = new WaterCausticsRenderer();
|
|
44
|
+
const _underwaterRenderer = new UnderwaterRenderer();
|
|
40
45
|
const _devicePoolAllocator = new DevicePoolAllocator();
|
|
41
46
|
const _textureAffinityCaches = new WeakMap();
|
|
42
47
|
function getTextureAffinityCache(camera, device) {
|
|
@@ -210,6 +215,21 @@ function getSSSLightingTextureFormat(ctx, attachmentCount, surfaceAttachmentCoun
|
|
|
210
215
|
function getFullMipLevelCount(width, height) {
|
|
211
216
|
return Math.max(1, Math.floor(Math.log2(Math.max(1, width, height))) + 1);
|
|
212
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Levels the scene colour copy is allocated with.
|
|
220
|
+
*
|
|
221
|
+
* Deep enough to serve the refraction blur, which clamps its own LOD, and no
|
|
222
|
+
* deeper: the coarsest levels of a full chain are averages of the whole frame,
|
|
223
|
+
* which is not a plausible answer for "what is behind this water", and every
|
|
224
|
+
* level costs bandwidth to generate on each grab. Capped against the full chain
|
|
225
|
+
* so a small render target stays legal.
|
|
226
|
+
*
|
|
227
|
+
* Derived from the water material's clamp rather than restated, so the two
|
|
228
|
+
* cannot drift: a level count one short of it would leave the sampler asking for
|
|
229
|
+
* a level that does not exist.
|
|
230
|
+
*/ function getSceneColorMipLevelCount(width, height) {
|
|
231
|
+
return Math.min(REFRACT_BLUR_MAX_LOD + 1, getFullMipLevelCount(width, height));
|
|
232
|
+
}
|
|
213
233
|
function hasSurfaceMRT(ctx) {
|
|
214
234
|
return !!(ctx.materialFlags & SURFACE_MRT_FLAGS);
|
|
215
235
|
}
|
|
@@ -244,10 +264,16 @@ function getLightPassColorAttachments(ctx, colorAttachment) {
|
|
|
244
264
|
const skinSSS = camera.skinSSS && renderQueueHasActiveSkinSSS(renderQueue);
|
|
245
265
|
const needSceneColor = renderQueue.needSceneColor();
|
|
246
266
|
const needSceneColorWithDepth = renderQueue.needSceneColorWithDepth();
|
|
267
|
+
// Requested by materials rather than by a post effect, so it is resolved here
|
|
268
|
+
// instead of in resolveFrameResourceRequirements. WebGL1 has no Hi-Z at all,
|
|
269
|
+
// and a material asking for one there would turn a missing feature into a
|
|
270
|
+
// thrown frame; the material is expected to shade without it.
|
|
271
|
+
const needHiZNearest = (renderQueue.needHiZNearest() || camera.HiZNearest) && deviceType !== 'webgl';
|
|
247
272
|
return {
|
|
248
273
|
depthPrepass: true,
|
|
249
274
|
motionVectors: false,
|
|
250
|
-
hiZ:
|
|
275
|
+
hiZ: needHiZNearest,
|
|
276
|
+
hiZNearest: needHiZNearest,
|
|
251
277
|
sceneNormal: false,
|
|
252
278
|
sceneRoughness: false,
|
|
253
279
|
shadowMask: false,
|
|
@@ -266,7 +292,10 @@ function getLightPassColorAttachments(ctx, colorAttachment) {
|
|
|
266
292
|
function resolveFrameResourceRequirements(ctx, options, requirements) {
|
|
267
293
|
const deviceType = ctx.device.type;
|
|
268
294
|
options.motionVectors ||= !!requirements.motionVector;
|
|
269
|
-
options.
|
|
295
|
+
options.hiZNearest ||= !!requirements.hiZNearest;
|
|
296
|
+
// The nearest channel lives on the same pyramid, so asking for it is asking
|
|
297
|
+
// for the pyramid.
|
|
298
|
+
options.hiZ ||= !!requirements.hiZ || options.hiZNearest;
|
|
270
299
|
options.sceneNormal ||= !!requirements.sceneNormal;
|
|
271
300
|
options.sceneRoughness ||= !!requirements.sceneRoughness;
|
|
272
301
|
options.shadowMask ||= !!requirements.shadowMask;
|
|
@@ -401,6 +430,152 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
401
430
|
});
|
|
402
431
|
}
|
|
403
432
|
};
|
|
433
|
+
/**
|
|
434
|
+
* Picks the water surface and light the caustic map is built from.
|
|
435
|
+
*
|
|
436
|
+
* Only one pair is handled per frame: the caustics ride on a single light's
|
|
437
|
+
* additive pass, and a second water body would need a second map. The largest
|
|
438
|
+
* visible water surface wins, which is the one a camera near several of them is
|
|
439
|
+
* most likely to be looking through.
|
|
440
|
+
* @internal
|
|
441
|
+
*/ function selectWaterCausticSource(ctx, renderQueue) {
|
|
442
|
+
if (ctx.device.type === 'webgl' || renderQueue.waters.length === 0) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
// The caustic light must own an additive shadow pass, because that is the pass
|
|
446
|
+
// the caustic uniforms are declared and bound on.
|
|
447
|
+
const candidate = renderQueue.sunLight ?? renderQueue.primaryDirectionalLight;
|
|
448
|
+
const light = candidate && candidate.castShadow && renderQueue.shadowedLights.indexOf(candidate) >= 0 ? candidate : null;
|
|
449
|
+
if (!light) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const waters = renderQueue.waters.filter((water)=>WaterCausticsRenderer.canRender(water, light));
|
|
453
|
+
if (waters.length === 0) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
// Largest first, which decides both what survives the cap and which body's
|
|
457
|
+
// settings describe the shared map. Area is a stand-in for "the one the viewer
|
|
458
|
+
// is most likely to be over"; a camera-distance sort would swap the map's
|
|
459
|
+
// whole configuration as the viewer walks between two pools, and swapping it
|
|
460
|
+
// discards the temporal history every time.
|
|
461
|
+
waters.sort((a, b)=>{
|
|
462
|
+
const ra = a.material.region;
|
|
463
|
+
const rb = b.material.region;
|
|
464
|
+
const areaA = Math.max(0, ra.z - ra.x) * Math.max(0, ra.w - ra.y);
|
|
465
|
+
const areaB = Math.max(0, rb.z - rb.x) * Math.max(0, rb.w - rb.y);
|
|
466
|
+
return areaB - areaA;
|
|
467
|
+
});
|
|
468
|
+
return {
|
|
469
|
+
waters: waters.slice(0, MAX_CAUSTIC_WATERS),
|
|
470
|
+
light
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/** @internal */ const WaterCausticsModule = {
|
|
474
|
+
type: 'WaterCaustics',
|
|
475
|
+
writes: [
|
|
476
|
+
FrameResources.WaterCaustics
|
|
477
|
+
],
|
|
478
|
+
prepare: ({ ctx, renderQueue })=>({
|
|
479
|
+
enabled: !!selectWaterCausticSource(ctx, renderQueue)
|
|
480
|
+
}),
|
|
481
|
+
setup (fg) {
|
|
482
|
+
const { graph, ctx, renderQueue, ordering, blackboard } = fg;
|
|
483
|
+
const source = selectWaterCausticSource(ctx, renderQueue);
|
|
484
|
+
// Map-wide settings come from the largest body; see selectWaterCausticSource.
|
|
485
|
+
const material = source.waters[0].material;
|
|
486
|
+
const size = material.causticsResolution;
|
|
487
|
+
const format = WaterCausticsRenderer.getMapFormat(ctx.device);
|
|
488
|
+
const history = fg.history;
|
|
489
|
+
const temporal = material.causticsTemporalStrength > 0;
|
|
490
|
+
const mapDesc = {
|
|
491
|
+
format,
|
|
492
|
+
sizeMode: 'absolute',
|
|
493
|
+
width: size,
|
|
494
|
+
height: size
|
|
495
|
+
};
|
|
496
|
+
const mapSize = {
|
|
497
|
+
width: size,
|
|
498
|
+
height: size
|
|
499
|
+
};
|
|
500
|
+
// Null on the first frame, after a resize, and whenever the map format
|
|
501
|
+
// changes; the renderer falls back to the current frame alone.
|
|
502
|
+
const previousHandle = temporal && history ? history.importPreviousIfCompatible(graph, RGHistoryResources.WATER_CAUSTICS, mapDesc, mapSize) : null;
|
|
503
|
+
const result = graph.addPass('WaterCaustics', (builder)=>{
|
|
504
|
+
// The wave textures the splat pass samples are updated by the scene, not by
|
|
505
|
+
// the graph, so chain after the passes that have already run rather than
|
|
506
|
+
// declaring a resource dependency on them.
|
|
507
|
+
ordering.chainInto(builder);
|
|
508
|
+
ordering.emit(builder, 'WaterCausticsDone');
|
|
509
|
+
// The map leaves the graph through the draw context rather than through a
|
|
510
|
+
// handle a later pass samples, so nothing would otherwise keep this alive.
|
|
511
|
+
builder.sideEffect();
|
|
512
|
+
// Fixed-size, unlike almost every other graph texture: the map covers a
|
|
513
|
+
// world-space footprint, not the screen.
|
|
514
|
+
const mapHandle = builder.createTexture({
|
|
515
|
+
format,
|
|
516
|
+
sizeMode: 'absolute',
|
|
517
|
+
width: size,
|
|
518
|
+
height: size,
|
|
519
|
+
label: 'waterCaustics',
|
|
520
|
+
allocationKey: 'ForwardPlus.WaterCaustics'
|
|
521
|
+
});
|
|
522
|
+
const scratchHandle = builder.createTexture({
|
|
523
|
+
format,
|
|
524
|
+
sizeMode: 'absolute',
|
|
525
|
+
width: size,
|
|
526
|
+
height: size,
|
|
527
|
+
label: 'waterCausticsScratch',
|
|
528
|
+
allocationKey: 'ForwardPlus.WaterCausticsScratch'
|
|
529
|
+
});
|
|
530
|
+
// The displaced surface height along each map ray, which the receiver
|
|
531
|
+
// gates its depth on. Same slice and size as the map so it shares the
|
|
532
|
+
// map's uv; four channels, one per water slot.
|
|
533
|
+
const heightHandle = builder.createTexture({
|
|
534
|
+
format: WaterCausticsRenderer.getHeightMapFormat(),
|
|
535
|
+
sizeMode: 'absolute',
|
|
536
|
+
width: size,
|
|
537
|
+
height: size,
|
|
538
|
+
label: 'waterCausticHeights',
|
|
539
|
+
allocationKey: 'ForwardPlus.WaterCausticHeights'
|
|
540
|
+
});
|
|
541
|
+
if (previousHandle) {
|
|
542
|
+
builder.read(previousHandle);
|
|
543
|
+
}
|
|
544
|
+
builder.setExecute((rgCtx)=>{
|
|
545
|
+
const map = rgCtx.getTexture(mapHandle);
|
|
546
|
+
const scratch = rgCtx.getTexture(scratchHandle);
|
|
547
|
+
const heights = rgCtx.getTexture(heightHandle);
|
|
548
|
+
const previous = previousHandle ? rgCtx.getTexture(previousHandle) : null;
|
|
549
|
+
const resolved = _waterCausticsRenderer.render(ctx, source.waters, source.light, map, scratch, heights, previous, (texture)=>rgCtx.createFramebuffer({
|
|
550
|
+
width: texture.width,
|
|
551
|
+
height: texture.height,
|
|
552
|
+
colorAttachments: texture,
|
|
553
|
+
depthAttachment: null
|
|
554
|
+
}));
|
|
555
|
+
if (temporal && history) {
|
|
556
|
+
// Commit whichever texture the renderer settled on, so the next frame
|
|
557
|
+
// reprojects the map that was actually shown rather than the
|
|
558
|
+
// pre-resolve one.
|
|
559
|
+
history.queueRetainedCommit(RGHistoryResources.WATER_CAUSTICS, mapDesc, mapSize, resolved);
|
|
560
|
+
}
|
|
561
|
+
// Publish only after the map exists: the light pass keys its shader
|
|
562
|
+
// variant on this flag and binds the texture in the same step.
|
|
563
|
+
ctx.waterCaustics = true;
|
|
564
|
+
ctx.waterCausticLight = source.light;
|
|
565
|
+
ctx.waterCausticTexture = resolved;
|
|
566
|
+
ctx.waterCausticHeightTexture = heights;
|
|
567
|
+
ctx.waterCausticUniforms = _waterCausticsRenderer.uniforms;
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
mapHandle,
|
|
571
|
+
scratchHandle
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
// Which target holds the result depends on whether the resolve ran. Both are
|
|
575
|
+
// declared by this pass, so either is a valid handle for consumers to read.
|
|
576
|
+
blackboard.set(FrameResources.WaterCaustics, temporal && previousHandle ? result.scratchHandle : result.mapHandle);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
404
579
|
/** @internal */ const DepthPrepassModule = {
|
|
405
580
|
type: 'DepthPrepass',
|
|
406
581
|
writes: [
|
|
@@ -580,7 +755,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
580
755
|
enabled: options.hiZ
|
|
581
756
|
}),
|
|
582
757
|
setup (fg) {
|
|
583
|
-
const { graph, ctx, frame, blackboard } = fg;
|
|
758
|
+
const { graph, ctx, frame, blackboard, options } = fg;
|
|
584
759
|
const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'HiZ');
|
|
585
760
|
const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
|
|
586
761
|
//const hiZWidth = nextPowerOf2(ctx.renderWidth);
|
|
@@ -593,11 +768,14 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
593
768
|
builder.read(preLightTransmissionDepthToken);
|
|
594
769
|
}
|
|
595
770
|
hiZHandle = builder.createTexture({
|
|
596
|
-
format:
|
|
771
|
+
format: getHiZFormat(options.hiZNearest),
|
|
597
772
|
label: 'hiZ',
|
|
598
773
|
sizeMode: 'backbuffer-relative',
|
|
599
774
|
mipLevels: getFullMipLevelCount(ctx.renderWidth, ctx.renderHeight),
|
|
600
|
-
|
|
775
|
+
// Keyed on the channel count: a one- and a two-channel pyramid are
|
|
776
|
+
// different textures, and pooling them under one key would hand a
|
|
777
|
+
// consumer a pyramid with no nearest channel in it.
|
|
778
|
+
allocationKey: `ForwardPlus.HiZ${options.hiZNearest ? '.MinMax' : ''}`
|
|
601
779
|
});
|
|
602
780
|
const hiZFramebufferHandle = builder.createFramebuffer({
|
|
603
781
|
label: 'HiZFramebuffer',
|
|
@@ -633,6 +811,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
633
811
|
const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SSSProfile');
|
|
634
812
|
const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
|
|
635
813
|
const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
|
|
814
|
+
const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
|
|
636
815
|
const renderDepthAttachment = fg.state.renderDepthAttachment;
|
|
637
816
|
const sssProfileResult = graph.addPass('SSSProfile', (builder)=>{
|
|
638
817
|
builder.read(blackboard.expect(FrameResources.LinearDepth));
|
|
@@ -643,6 +822,12 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
643
822
|
if (shadowMaskHandle) {
|
|
644
823
|
builder.read(shadowMaskHandle);
|
|
645
824
|
}
|
|
825
|
+
if (waterCausticsHandle) {
|
|
826
|
+
// Same reason as the shadow mask: the map reaches the shader through
|
|
827
|
+
// ctx.waterCausticTexture, so only a declared read stops the executor
|
|
828
|
+
// from recycling it while this pass is still sampling it.
|
|
829
|
+
builder.read(waterCausticsHandle);
|
|
830
|
+
}
|
|
646
831
|
const profileHandle = builder.createTexture({
|
|
647
832
|
format: 'rgba16f',
|
|
648
833
|
label: 'sssProfile',
|
|
@@ -691,6 +876,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
691
876
|
const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SceneColorGrab');
|
|
692
877
|
const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
|
|
693
878
|
const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
|
|
879
|
+
const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
|
|
694
880
|
const renderDepthAttachment = fg.state.renderDepthAttachment;
|
|
695
881
|
const grabResult = graph.addPass('SceneColorGrab', (builder)=>{
|
|
696
882
|
builder.read(blackboard.expect(FrameResources.LinearDepth));
|
|
@@ -701,9 +887,34 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
701
887
|
if (shadowMaskHandle) {
|
|
702
888
|
builder.read(shadowMaskHandle);
|
|
703
889
|
}
|
|
890
|
+
if (waterCausticsHandle) {
|
|
891
|
+
// Same reason as the shadow mask: the map reaches the shader through
|
|
892
|
+
// ctx.waterCausticTexture, so only a declared read stops the executor
|
|
893
|
+
// from recycling it while this pass is still sampling it.
|
|
894
|
+
builder.read(waterCausticsHandle);
|
|
895
|
+
}
|
|
896
|
+
// A few mip levels, because the refraction background is sampled at a LOD
|
|
897
|
+
// and not only at mip 0: water picks one from how deep and how turbid the
|
|
898
|
+
// column is, so a stone under clear shallows stays sharp while the same
|
|
899
|
+
// stone under metres of silt smears. With a single-level texture the
|
|
900
|
+
// sampler returned mip 0 whatever LOD was asked for, and the blur did
|
|
901
|
+
// nothing.
|
|
902
|
+
//
|
|
903
|
+
// Nothing here generates the chain - both backends do it when the
|
|
904
|
+
// framebuffer is unbound, for any colour attachment with more than one
|
|
905
|
+
// level (WebGL `framebuffer_webgl.ts` unbind, WebGPU `renderpass_webgpu.ts`
|
|
906
|
+
// end), and `generateMipmaps` defaults to true on colour attachments. The
|
|
907
|
+
// level count is the whole precondition.
|
|
908
|
+
//
|
|
909
|
+
// Not the full chain down to 1x1. The consumer clamps its LOD, and the
|
|
910
|
+
// coarsest levels of a full chain average across the whole frame - a
|
|
911
|
+
// sample from one stops being "what is behind the water" and becomes the
|
|
912
|
+
// mean of the image. Generating them would cost bandwidth every frame to
|
|
913
|
+
// produce levels nothing may read.
|
|
704
914
|
const copyHandle = builder.createTexture({
|
|
705
915
|
format: ctx.colorFormat,
|
|
706
916
|
label: 'sceneColorCopy',
|
|
917
|
+
mipLevels: getSceneColorMipLevelCount(ctx.renderWidth, ctx.renderHeight),
|
|
707
918
|
allocationKey: 'ForwardPlus.SceneColorCopy'
|
|
708
919
|
});
|
|
709
920
|
// Isolate depth when SSR inserts transmission depth before LightPass.
|
|
@@ -744,6 +955,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
744
955
|
const { graph, ctx, frame, blackboard, options, backbuffer } = fg;
|
|
745
956
|
const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'LightPass');
|
|
746
957
|
const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
|
|
958
|
+
const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
|
|
747
959
|
const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
|
|
748
960
|
const hiZHandle = blackboard.get(FrameResources.HiZ);
|
|
749
961
|
const sssProfileHandle = blackboard.get(FrameResources.SSSProfile);
|
|
@@ -814,6 +1026,12 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
814
1026
|
if (shadowMaskHandle) {
|
|
815
1027
|
builder.read(shadowMaskHandle);
|
|
816
1028
|
}
|
|
1029
|
+
if (waterCausticsHandle) {
|
|
1030
|
+
// Same reason as the shadow mask: the map reaches the shader through
|
|
1031
|
+
// ctx.waterCausticTexture, so only a declared read stops the executor
|
|
1032
|
+
// from recycling it while this pass is still sampling it.
|
|
1033
|
+
builder.read(waterCausticsHandle);
|
|
1034
|
+
}
|
|
817
1035
|
if (preLightTransmissionDepthToken) {
|
|
818
1036
|
builder.read(preLightTransmissionDepthToken);
|
|
819
1037
|
}
|
|
@@ -1001,6 +1219,68 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
1001
1219
|
}
|
|
1002
1220
|
}
|
|
1003
1221
|
};
|
|
1222
|
+
/** @internal */ const UnderwaterModule = {
|
|
1223
|
+
type: 'Underwater',
|
|
1224
|
+
reads: [
|
|
1225
|
+
{
|
|
1226
|
+
resource: FrameResources.SceneColor,
|
|
1227
|
+
version: 'current'
|
|
1228
|
+
}
|
|
1229
|
+
],
|
|
1230
|
+
writes: [
|
|
1231
|
+
FrameResources.SceneColor
|
|
1232
|
+
],
|
|
1233
|
+
prepare: ({ ctx })=>({
|
|
1234
|
+
enabled: !!ctx.underwater
|
|
1235
|
+
}),
|
|
1236
|
+
setup (fg) {
|
|
1237
|
+
const { graph, ctx, frame, blackboard } = fg;
|
|
1238
|
+
const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'Underwater');
|
|
1239
|
+
const lightPassResult = requireBuildState(fg, 'lightPass', 'LightPass', 'Underwater');
|
|
1240
|
+
const sceneColorHandle = blackboard.expect(FrameResources.SceneColor);
|
|
1241
|
+
const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
|
|
1242
|
+
const state = ctx.underwater;
|
|
1243
|
+
// After the sky, before the transparent pass: the medium has to cover the
|
|
1244
|
+
// sky - that is what the horizon fades into underwater - and the opaque
|
|
1245
|
+
// scene is the only thing the depth buffer describes, so it is also the only
|
|
1246
|
+
// thing this can reach. Transparent geometry is on its own, exactly as it is
|
|
1247
|
+
// for atmospheric fog.
|
|
1248
|
+
const underwaterColorHandle = graph.addPass('Underwater', (builder)=>{
|
|
1249
|
+
builder.read(sceneColorHandle);
|
|
1250
|
+
builder.read(depthPassResult.depthFramebufferHandle);
|
|
1251
|
+
if (lightPassResult.sceneColorFramebufferHandle) {
|
|
1252
|
+
builder.read(lightPassResult.sceneColorFramebufferHandle);
|
|
1253
|
+
}
|
|
1254
|
+
if (waterCausticsHandle) {
|
|
1255
|
+
// The light shafts sample the caustic map, which reaches the shader
|
|
1256
|
+
// through ctx.waterCausticTexture rather than through a handle. Same
|
|
1257
|
+
// lifetime argument as the shadow mask in CompositeTail: only a declared
|
|
1258
|
+
// read stops the executor returning it to the pool after LightPass.
|
|
1259
|
+
builder.read(waterCausticsHandle);
|
|
1260
|
+
}
|
|
1261
|
+
const out = builder.write(sceneColorHandle);
|
|
1262
|
+
builder.setExecute((rgCtx)=>{
|
|
1263
|
+
const colorTexture = rgCtx.getTexture(sceneColorHandle);
|
|
1264
|
+
const depthTexture = frame.depthFramebuffer?.getDepthAttachment();
|
|
1265
|
+
if (!colorTexture || !depthTexture) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
// Color only. The scene depth is sampled by this pass, which it cannot
|
|
1269
|
+
// be while it is still attached to the bound framebuffer.
|
|
1270
|
+
const colorFramebuffer = rgCtx.createFramebuffer({
|
|
1271
|
+
width: colorTexture.width,
|
|
1272
|
+
height: colorTexture.height,
|
|
1273
|
+
colorAttachments: colorTexture,
|
|
1274
|
+
depthAttachment: null
|
|
1275
|
+
});
|
|
1276
|
+
_underwaterRenderer.render(ctx, state, colorFramebuffer, depthTexture);
|
|
1277
|
+
});
|
|
1278
|
+
return out;
|
|
1279
|
+
});
|
|
1280
|
+
lightPassResult.sceneColorHandle = underwaterColorHandle;
|
|
1281
|
+
blackboard.set(FrameResources.SceneColor, underwaterColorHandle);
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1004
1284
|
/** @internal */ const CompositeTailModule = {
|
|
1005
1285
|
type: 'CompositeTail',
|
|
1006
1286
|
writes: [
|
|
@@ -1017,6 +1297,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
1017
1297
|
const hiZHandle = blackboard.get(FrameResources.HiZ);
|
|
1018
1298
|
const sceneColorCopyHandle = blackboard.get(FrameResources.SceneColorCopy);
|
|
1019
1299
|
const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
|
|
1300
|
+
const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
|
|
1020
1301
|
const lightPassResult = requireBuildState(fg, 'lightPass', 'LightPass', 'CompositeTail');
|
|
1021
1302
|
const renderDepthAttachment = fg.state.renderDepthAttachment;
|
|
1022
1303
|
const useFinalFramebufferAsIntermediate = fg.state.useFinalFramebufferAsIntermediate;
|
|
@@ -1090,6 +1371,10 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
1090
1371
|
// after LightPass while this pass is still sampling it.
|
|
1091
1372
|
builder.read(shadowMaskHandle);
|
|
1092
1373
|
}
|
|
1374
|
+
if (waterCausticsHandle) {
|
|
1375
|
+
// Same lifetime argument as the shadow mask above.
|
|
1376
|
+
builder.read(waterCausticsHandle);
|
|
1377
|
+
}
|
|
1093
1378
|
builder.read(opaqueChainResult.color);
|
|
1094
1379
|
const out = builder.write(opaqueChainResult.color);
|
|
1095
1380
|
builder.setExecute((rgCtx)=>{
|
|
@@ -1205,6 +1490,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
1205
1490
|
ClusterLights: ClusterLightsModule,
|
|
1206
1491
|
GPUPicking: GPUPickingModule,
|
|
1207
1492
|
ShadowMaps: ShadowMapsModule,
|
|
1493
|
+
WaterCaustics: WaterCausticsModule,
|
|
1208
1494
|
DepthPrepass: DepthPrepassModule,
|
|
1209
1495
|
ShadowMask: ShadowMaskModule,
|
|
1210
1496
|
TransmissionDepthForSSR: TransmissionDepthForSSRModule,
|
|
@@ -1213,6 +1499,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
1213
1499
|
SceneColorGrab: SceneColorGrabModule,
|
|
1214
1500
|
LightPass: LightPassModule,
|
|
1215
1501
|
SkyPass: SkyPassModule,
|
|
1502
|
+
Underwater: UnderwaterModule,
|
|
1216
1503
|
CompositeTail: CompositeTailModule
|
|
1217
1504
|
};
|
|
1218
1505
|
/** The default module order assembled by {@link createForwardPlusPipeline}. */ const DEFAULT_FORWARD_PLUS_MODULES = [
|
|
@@ -1220,6 +1507,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
1220
1507
|
ClusterLightsModule,
|
|
1221
1508
|
GPUPickingModule,
|
|
1222
1509
|
ShadowMapsModule,
|
|
1510
|
+
WaterCausticsModule,
|
|
1223
1511
|
DepthPrepassModule,
|
|
1224
1512
|
ShadowMaskModule,
|
|
1225
1513
|
TransmissionDepthForSSRModule,
|
|
@@ -1228,6 +1516,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
|
|
|
1228
1516
|
SceneColorGrabModule,
|
|
1229
1517
|
LightPassModule,
|
|
1230
1518
|
SkyPassModule,
|
|
1519
|
+
UnderwaterModule,
|
|
1231
1520
|
CompositeTailModule
|
|
1232
1521
|
];
|
|
1233
1522
|
/** @internal */ let _defaultForwardPlusPipeline = null;
|
|
@@ -1255,6 +1544,13 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
|
|
|
1255
1544
|
ctx.SkinSSSTexture = null;
|
|
1256
1545
|
// ShadowMask sets this only when it produces a texture.
|
|
1257
1546
|
ctx.shadowMaskTexture = null;
|
|
1547
|
+
// WaterCaustics turns these on together once it has produced a map; they are
|
|
1548
|
+
// read by every light pass, so they must start cleared each frame.
|
|
1549
|
+
ctx.waterCaustics = false;
|
|
1550
|
+
ctx.waterCausticLight = null;
|
|
1551
|
+
ctx.waterCausticTexture = null;
|
|
1552
|
+
ctx.waterCausticHeightTexture = null;
|
|
1553
|
+
ctx.waterCausticUniforms = null;
|
|
1258
1554
|
const blackboard = new RGBlackboard();
|
|
1259
1555
|
const frame = {
|
|
1260
1556
|
ctx,
|
|
@@ -1740,7 +2036,11 @@ function blitToCurrentColorAttachment(ctx, source) {
|
|
|
1740
2036
|
device.setViewport(null);
|
|
1741
2037
|
device.setScissor(null);
|
|
1742
2038
|
ctx.scene.env.sky.renderSky(ctx);
|
|
1743
|
-
|
|
2039
|
+
// Not while submerged: the medium between the eye and the scene is water,
|
|
2040
|
+
// and the Underwater module composites it. The sky itself is still drawn -
|
|
2041
|
+
// that pass then extinguishes it along with everything else at the far
|
|
2042
|
+
// plane, which is what makes the horizon read as water rather than as air.
|
|
2043
|
+
if (ctx.scene.env.sky.fogPresents && !ctx.underwater) {
|
|
1744
2044
|
// Snapshot between sky and fog. Sky belongs in the copy - a ray that hits
|
|
1745
2045
|
// the sky should read the sky - while fog does not, because the fog a
|
|
1746
2046
|
// screen-space pass would sample lies along the camera ray rather than the
|
|
@@ -1822,8 +2122,17 @@ function blitToCurrentColorAttachment(ctx, source) {
|
|
|
1822
2122
|
let motionVectorFrameCommitted = false;
|
|
1823
2123
|
try {
|
|
1824
2124
|
renderQueue = _scenePass.cullScene(ctx, ctx.camera);
|
|
2125
|
+
// Before the options are derived, because being submerged changes them: the
|
|
2126
|
+
// atmosphere is not the medium the camera is looking through, so its fog is
|
|
2127
|
+
// suppressed for the frame.
|
|
2128
|
+
ctx.underwater = selectUnderwaterSource(ctx);
|
|
1825
2129
|
const options = deriveForwardPlusOptions(ctx.scene, ctx.camera, device.type, renderQueue);
|
|
1826
2130
|
options.ssgi &&= supportsSSGIRenderTargets(ctx);
|
|
2131
|
+
// Atmospheric fog describes a column of air, which is not what a submerged
|
|
2132
|
+
// camera is looking through. The Underwater module owns the medium instead.
|
|
2133
|
+
// Clearing the flag here also stops SkyPass reserving a fog-free snapshot
|
|
2134
|
+
// for a frame that composites no fog.
|
|
2135
|
+
options.fogPresents &&= !ctx.underwater;
|
|
1827
2136
|
ctx.SSS = options.sss;
|
|
1828
2137
|
historyManager = ctx.camera.getHistoryResourceManager();
|
|
1829
2138
|
if (!historyManager) {
|