@zephyr3d/scene 0.9.18 → 0.9.20

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.
Files changed (124) hide show
  1. package/dist/animation/joint_dynamics/convex_collider.js +320 -0
  2. package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
  3. package/dist/app/scriptregistry.js +124 -6
  4. package/dist/app/scriptregistry.js.map +1 -1
  5. package/dist/asset/assetmanager.js +1 -0
  6. package/dist/asset/assetmanager.js.map +1 -1
  7. package/dist/camera/camera.js +240 -201
  8. package/dist/camera/camera.js.map +1 -1
  9. package/dist/index.d.ts +458 -9
  10. package/dist/index.js +4 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/material/meshmaterial.js +2 -2
  13. package/dist/material/meshmaterial.js.map +1 -1
  14. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +11 -5
  15. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  16. package/dist/material/mixins/lit.js +10 -0
  17. package/dist/material/mixins/lit.js.map +1 -1
  18. package/dist/material/shader/helper.js +138 -10
  19. package/dist/material/shader/helper.js.map +1 -1
  20. package/dist/material/skin.js +4 -6
  21. package/dist/material/skin.js.map +1 -1
  22. package/dist/material/water.js +1 -1
  23. package/dist/material/water.js.map +1 -1
  24. package/dist/posteffect/bloom.js +177 -29
  25. package/dist/posteffect/bloom.js.map +1 -1
  26. package/dist/posteffect/compositor.js +111 -204
  27. package/dist/posteffect/compositor.js.map +1 -1
  28. package/dist/posteffect/motionblur.js +3 -0
  29. package/dist/posteffect/motionblur.js.map +1 -1
  30. package/dist/posteffect/posteffect.js +79 -0
  31. package/dist/posteffect/posteffect.js.map +1 -1
  32. package/dist/posteffect/skinsss.js.map +1 -1
  33. package/dist/posteffect/ssr.js +238 -7
  34. package/dist/posteffect/ssr.js.map +1 -1
  35. package/dist/posteffect/sss.js +16 -15
  36. package/dist/posteffect/sss.js.map +1 -1
  37. package/dist/posteffect/taa.js +121 -19
  38. package/dist/posteffect/taa.js.map +1 -1
  39. package/dist/posteffect/tonemap.js +1 -1
  40. package/dist/posteffect/tonemap.js.map +1 -1
  41. package/dist/render/abuffer_oit.js +7 -1
  42. package/dist/render/abuffer_oit.js.map +1 -1
  43. package/dist/render/cluster_light.js +52 -8
  44. package/dist/render/cluster_light.js.map +1 -1
  45. package/dist/render/dualdepthpeeling_oit.js +331 -0
  46. package/dist/render/dualdepthpeeling_oit.js.map +1 -0
  47. package/dist/render/envlight.js +6 -3
  48. package/dist/render/envlight.js.map +1 -1
  49. package/dist/render/hzb.js +11 -4
  50. package/dist/render/hzb.js.map +1 -1
  51. package/dist/render/lightpass.js +21 -12
  52. package/dist/render/lightpass.js.map +1 -1
  53. package/dist/render/renderer.js +7 -3
  54. package/dist/render/renderer.js.map +1 -1
  55. package/dist/render/rendergraph/blackboard.js +79 -0
  56. package/dist/render/rendergraph/blackboard.js.map +1 -0
  57. package/dist/render/rendergraph/device_pool_allocator.js +10 -1
  58. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  59. package/dist/render/rendergraph/executor.js +41 -11
  60. package/dist/render/rendergraph/executor.js.map +1 -1
  61. package/dist/render/rendergraph/forward_plus_builder.js +551 -201
  62. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  63. package/dist/render/rendergraph/rendergraph.js +29 -0
  64. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  65. package/dist/render/rendergraph/types.js.map +1 -1
  66. package/dist/render/renderpass.js +12 -2
  67. package/dist/render/renderpass.js.map +1 -1
  68. package/dist/render/shadow_mask_pass.js +297 -0
  69. package/dist/render/shadow_mask_pass.js.map +1 -0
  70. package/dist/render/weightedblended_oit.js +5 -0
  71. package/dist/render/weightedblended_oit.js.map +1 -1
  72. package/dist/scene/basesprite.js +1 -0
  73. package/dist/scene/basesprite.js.map +1 -1
  74. package/dist/scene/batchgroup.js +1 -0
  75. package/dist/scene/batchgroup.js.map +1 -1
  76. package/dist/scene/environment.js +1 -0
  77. package/dist/scene/environment.js.map +1 -1
  78. package/dist/scene/msdftext.js +1 -0
  79. package/dist/scene/msdftext.js.map +1 -1
  80. package/dist/scene/msdftextsprite.js +1 -0
  81. package/dist/scene/msdftextsprite.js.map +1 -1
  82. package/dist/scene/particlesys.js +1 -0
  83. package/dist/scene/particlesys.js.map +1 -1
  84. package/dist/scene/scene.js +1 -0
  85. package/dist/scene/scene.js.map +1 -1
  86. package/dist/scene/terrain-cm/grass.js +1 -0
  87. package/dist/scene/terrain-cm/grass.js.map +1 -1
  88. package/dist/scene/terrain-cm/terrain-cm.js +1 -0
  89. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  90. package/dist/scene/water.js +1 -0
  91. package/dist/scene/water.js.map +1 -1
  92. package/dist/shaders/ssr.js +52 -21
  93. package/dist/shaders/ssr.js.map +1 -1
  94. package/dist/utility/misc.js +10 -1
  95. package/dist/utility/misc.js.map +1 -1
  96. package/dist/utility/serialization/manager.js +1 -0
  97. package/dist/utility/serialization/manager.js.map +1 -1
  98. package/dist/utility/serialization/scene/animation.js +1 -0
  99. package/dist/utility/serialization/scene/animation.js.map +1 -1
  100. package/dist/utility/serialization/scene/batch.js +1 -0
  101. package/dist/utility/serialization/scene/batch.js.map +1 -1
  102. package/dist/utility/serialization/scene/camera.js +35 -2
  103. package/dist/utility/serialization/scene/camera.js.map +1 -1
  104. package/dist/utility/serialization/scene/light.js +1 -0
  105. package/dist/utility/serialization/scene/light.js.map +1 -1
  106. package/dist/utility/serialization/scene/mesh.js +1 -0
  107. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  108. package/dist/utility/serialization/scene/node.js +1 -0
  109. package/dist/utility/serialization/scene/node.js.map +1 -1
  110. package/dist/utility/serialization/scene/particle.js +1 -0
  111. package/dist/utility/serialization/scene/particle.js.map +1 -1
  112. package/dist/utility/serialization/scene/primitive.js +1 -0
  113. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  114. package/dist/utility/serialization/scene/sprite.js +1 -0
  115. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  116. package/dist/utility/serialization/scene/terrain.js +1 -0
  117. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  118. package/dist/utility/serialization/scene/text.js +1 -0
  119. package/dist/utility/serialization/scene/text.js.map +1 -1
  120. package/dist/utility/serialization/scene/water.js +1 -0
  121. package/dist/utility/serialization/scene/water.js.map +1 -1
  122. package/dist/values.js +11 -1
  123. package/dist/values.js.map +1 -1
  124. package/package.json +2 -2
@@ -4,18 +4,20 @@ 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
+ import { ShadowMaskRenderer } from '../shadow_mask_pass.js';
7
8
  import { buildHiZ } from '../hzb.js';
8
9
  import '../primitive.js';
9
10
  import '@zephyr3d/device';
10
11
  import { CopyBlitter } from '../../blitter/copy.js';
11
12
  import { fetchSampler } from '../../utility/misc.js';
12
13
  import { MaterialVaryingFlags } from '../../values.js';
13
- import { AbstractPostEffect, PostEffectLayer } from '../../posteffect/posteffect.js';
14
+ import { PostEffectLayer, AbstractPostEffect } from '../../posteffect/posteffect.js';
14
15
  import { RenderGraph } from './rendergraph.js';
15
16
  import { RenderGraphExecutor } from './executor.js';
16
17
  import { DevicePoolAllocator } from './device_pool_allocator.js';
17
18
  import { HistoryResourceManager } from './history_resource_manager.js';
18
19
  import { RGHistoryResources } from './history_resources.js';
20
+ import { FrameResources, RGBlackboard } from './blackboard.js';
19
21
  import { renderObjectColors } from '../gpu_picking.js';
20
22
  import { BoxShape } from '../../shapes/box.js';
21
23
  import '../../shapes/capsule.js';
@@ -30,6 +32,7 @@ const _scenePass = new LightPass();
30
32
  const _depthPass = new DepthPass();
31
33
  const _shadowMapPass = new ShadowMapPass();
32
34
  const _clusters = [];
35
+ const _shadowMaskRenderer = new ShadowMaskRenderer();
33
36
  const _devicePoolAllocator = new DevicePoolAllocator();
34
37
  let _backDepthColorState = null;
35
38
  let _frontDepthColorState = null;
@@ -173,9 +176,11 @@ function getSSSLightingTextureFormat(ctx, attachmentCount, includeSSRSurfaceMRT)
173
176
  return colorFormat;
174
177
  }
175
178
  const caps = ctx.device.getDeviceCaps();
176
- const roughnessFormat = ctx.SSRRoughnessTexture?.format ?? colorFormat;
177
- const normalFormat = ctx.SSRNormalTexture?.format ?? colorFormat;
178
- const colorBytes = getTextureFormatBytes(ctx, colorFormat) + getTextureFormatBytes(ctx, roughnessFormat) + getTextureFormatBytes(ctx, normalFormat);
179
+ // The SSR roughness/normal MRT textures use the glossy surface format; they
180
+ // are graph textures now, so derive the format directly instead of reading
181
+ // the (not yet resolved) DrawContext fields.
182
+ const surfaceFormat = getSurfaceTextureFormat(ctx);
183
+ const colorBytes = getTextureFormatBytes(ctx, colorFormat) + getTextureFormatBytes(ctx, surfaceFormat) + getTextureFormatBytes(ctx, surfaceFormat);
179
184
  const fullPrecisionBytes = colorBytes + getTextureFormatBytes(ctx, colorFormat) * attachmentCount;
180
185
  if (fullPrecisionBytes <= caps.framebufferCaps.maxColorAttachmentBytesPerSample) {
181
186
  return colorFormat;
@@ -256,6 +261,8 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
256
261
  const backbuffer = graph.importTexture('backbuffer');
257
262
  ctx.SSS = !!options.sss;
258
263
  ctx.SkinSSSTexture = null;
264
+ // Named registry of shared frame resources (consumed by post effect setup)
265
+ const blackboard = new RGBlackboard();
259
266
  // Shared mutable frame state
260
267
  const frame = {
261
268
  ctx,
@@ -263,7 +270,6 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
263
270
  depthFramebuffer: null,
264
271
  sunLightColor: null,
265
272
  options,
266
- intermediateDeviceStatePushed: false,
267
273
  renderQueueDisposed: false,
268
274
  clusteredLightReleased: false,
269
275
  sunLightRestored: false
@@ -372,21 +378,99 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
372
378
  depthFramebufferHandle
373
379
  };
374
380
  });
375
- const depthHandle = depthPassResult.depthHandle;
381
+ let depthHandle = depthPassResult.depthHandle;
376
382
  const motionVectorHandle = depthPassResult.motionVectorHandle;
383
+ blackboard.set(FrameResources.LinearDepth, depthHandle);
384
+ if (motionVectorHandle) {
385
+ blackboard.set(FrameResources.MotionVector, motionVectorHandle);
386
+ }
387
+ if (depthPassResult.graphDepthAttachmentHandle) {
388
+ blackboard.set(FrameResources.SceneDepthAttachment, depthPassResult.graphDepthAttachmentHandle);
389
+ }
390
+ // ── Screen-space shadow mask ────────────────────────────────────────────
391
+ // When active, render each shadow-casting light's visibility into an RGBA8
392
+ // texture array (4 lights per layer). The clustered LightPass samples this
393
+ // mask instead of each shadowed light running its own additive pass. The
394
+ // layer/channel order is locked to ClusteredLight.getVisibleLights: shadow
395
+ // lights fill clustered buffer indices 1..N in renderQueue.shadowedLights
396
+ // order, and ordinal s = index-1 maps to layer s>>2, channel s&3.
397
+ let shadowMaskHandle = null;
398
+ // Reset any mask carried over from a previous frame; the pass below re-sets it
399
+ // during execution only when the mask is actually produced.
400
+ ctx.shadowMaskTexture = null;
401
+ // Gate on build-time state only: renderQueue.shadowedLights is available now,
402
+ // whereas ctx.shadowMapInfo is populated later by the ShadowMaps pass execute
403
+ // (which runs before this pass thanks to the orderToken chain), so it must not
404
+ // be part of the pass-creation condition.
405
+ const useShadowMask = ctx.screenSpaceShadowMask && renderQueue.shadowedLights.length > 0;
406
+ if (useShadowMask) {
407
+ const numShadowLights = renderQueue.shadowedLights.length;
408
+ const numLayers = ShadowMaskRenderer.getLayerCount(numShadowLights);
409
+ const maskPassResult = graph.addPass('ShadowMaskPass', (builder)=>{
410
+ builder.read(depthHandle);
411
+ builder.read(depthPassResult.depthFramebufferHandle);
412
+ // createTexture already registers this pass as the resource producer, so
413
+ // downstream passes read this handle directly (same pattern as HiZ). The
414
+ // per-layer framebuffers are created inside execute (rgCtx.createFramebuffer),
415
+ // which the graph cannot see; the createTexture producer edge is what keeps
416
+ // the resource alive and ordered before the LightPass reader.
417
+ const maskHandle = builder.createTexture({
418
+ format: 'rgba8unorm',
419
+ label: 'shadowMask',
420
+ arrayLayers: numLayers
421
+ });
422
+ builder.setExecute((rgCtx)=>{
423
+ const depthTex = rgCtx.getTexture(depthHandle);
424
+ const maskTex = rgCtx.getTexture(maskHandle);
425
+ _shadowMaskRenderer.render(ctx, depthTex, renderQueue.shadowedLights, (layer)=>rgCtx.createFramebuffer({
426
+ width: maskTex.width,
427
+ height: maskTex.height,
428
+ colorAttachments: maskTex,
429
+ depthAttachment: null,
430
+ attachmentLayer: layer
431
+ }));
432
+ ctx.shadowMaskTexture = maskTex;
433
+ });
434
+ return {
435
+ maskHandle
436
+ };
437
+ });
438
+ shadowMaskHandle = maskPassResult.maskHandle;
439
+ blackboard.set(FrameResources.ShadowMask, shadowMaskHandle);
440
+ }
377
441
  const renderDepthAttachment = depthPassResult.graphDepthAttachmentHandle ?? depthPassResult.externalDepthAttachment ?? null;
378
- const useFinalFramebufferAsIntermediate = !!depthPassResult.externalDepthAttachment && depthPassResult.externalDepthAttachment === ctx.finalFramebuffer?.getDepthAttachment();
442
+ // Rendering the scene directly into the final framebuffer is only possible
443
+ // when no opaque-layer effect is enabled: those effects must sample the
444
+ // opaque scene color as a texture and may require surface MRT attachments
445
+ // (SSR roughness / SSS / SkinSSS), which the single-color final framebuffer
446
+ // cannot carry. The legacy compositor.begin() used to redirect rendering to
447
+ // a temporal MRT framebuffer in that case; the graph path uses the regular
448
+ // scene color texture pipeline instead.
449
+ const opaqueLayerHasEffects = !!ctx.compositor?.layerHasEnabledEffect(PostEffectLayer.opaque);
450
+ const useFinalFramebufferAsIntermediate = !!depthPassResult.externalDepthAttachment && depthPassResult.externalDepthAttachment === ctx.finalFramebuffer?.getDepthAttachment() && !opaqueLayerHasEffects;
379
451
  let preLightTransmissionDepthToken;
380
452
  if (options.needsTransmissionDepthForSSR) {
381
- preLightTransmissionDepthToken = graph.addPass('TransmissionDepthForSSR', (builder)=>{
453
+ const transmissionDepthResult = graph.addPass('TransmissionDepthForSSR', (builder)=>{
454
+ builder.read(depthHandle);
382
455
  builder.read(depthPassResult.depthFramebufferHandle);
456
+ // This pass renders transmission geometry into the prepass linear-depth
457
+ // texture: model the mutation as a write so later readers order against
458
+ // it through data flow instead of relying on the token alone.
459
+ const depthOut = builder.write(depthHandle);
383
460
  const done = builder.createToken('TransmissionDepthForSSRDone');
384
461
  builder.sideEffect();
385
462
  builder.setExecute((rgCtx)=>{
386
463
  renderTransmissionDepthPass(frame, rgCtx);
387
464
  });
388
- return done;
465
+ return {
466
+ done,
467
+ depthOut
468
+ };
389
469
  });
470
+ preLightTransmissionDepthToken = transmissionDepthResult.done;
471
+ depthHandle = transmissionDepthResult.depthOut;
472
+ // Re-register so blackboard consumers read the post-transmission version.
473
+ blackboard.set(FrameResources.LinearDepth, depthHandle);
390
474
  }
391
475
  // ── 6. Hi-Z (optional) ───────────────────────────────────────────
392
476
  let hiZHandle;
@@ -398,7 +482,7 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
398
482
  builder.read(preLightTransmissionDepthToken);
399
483
  }
400
484
  hiZHandle = builder.createTexture({
401
- format: 'r32f',
485
+ format: 'rg32f',
402
486
  label: 'hiZ',
403
487
  mipLevels: getFullMipLevelCount(ctx.renderWidth, ctx.renderHeight)
404
488
  });
@@ -420,11 +504,13 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
420
504
  }
421
505
  });
422
506
  });
507
+ if (hiZHandle) {
508
+ blackboard.set(FrameResources.HiZ, hiZHandle);
509
+ }
423
510
  }
424
511
  // ── 7. Main Light Pass ────────────────────────────────────────────
425
512
  const historyManager = ctx.camera?.getHistoryResourceManager?.() ?? null;
426
513
  const lightHistoryReadBindings = [];
427
- const compositeHistoryReadBindings = [];
428
514
  const historySize = {
429
515
  width: ctx.renderWidth,
430
516
  height: ctx.renderHeight
@@ -452,29 +538,7 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
452
538
  });
453
539
  }
454
540
  }
455
- if (historyManager && ctx.camera?.TAA && options.motionVectors) {
456
- const colorHistoryHandle = historyManager.importPreviousIfCompatible(graph, RGHistoryResources.TAA_COLOR, {
457
- format: ctx.colorFormat,
458
- sizeMode: 'absolute',
459
- width: ctx.renderWidth,
460
- height: ctx.renderHeight
461
- }, historySize);
462
- const motionVectorHistoryHandle = historyManager.importPreviousIfCompatible(graph, RGHistoryResources.TAA_MOTION_VECTOR, {
463
- format: 'rgba16f',
464
- sizeMode: 'absolute',
465
- width: ctx.renderWidth,
466
- height: ctx.renderHeight
467
- }, historySize);
468
- if (colorHistoryHandle && motionVectorHistoryHandle) {
469
- compositeHistoryReadBindings.push({
470
- name: RGHistoryResources.TAA_COLOR,
471
- handle: colorHistoryHandle
472
- }, {
473
- name: RGHistoryResources.TAA_MOTION_VECTOR,
474
- handle: motionVectorHistoryHandle
475
- });
476
- }
477
- }
541
+ // Note: TAA history import/commit is handled by TAA.setup() (self-describing).
478
542
  let sssProfileResult;
479
543
  if (options.sss) {
480
544
  sssProfileResult = graph.addPass('SSSProfile', (builder)=>{
@@ -524,9 +588,47 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
524
588
  };
525
589
  });
526
590
  }
591
+ // 7b. Scene color grab: renders the full scene (no transmission) into a copy
592
+ // texture that transmission/refraction materials sample as background.
593
+ // Extracted from the former monolithic light pass.
594
+ let grabResult;
595
+ if (options.needSceneColor) {
596
+ grabResult = graph.addPass('SceneColorGrab', (builder)=>{
597
+ builder.read(depthHandle);
598
+ builder.read(depthPassResult.depthFramebufferHandle);
599
+ if (preLightTransmissionDepthToken) {
600
+ builder.read(preLightTransmissionDepthToken);
601
+ }
602
+ const copyHandle = builder.createTexture({
603
+ format: ctx.colorFormat,
604
+ label: 'sceneColorCopy'
605
+ });
606
+ // SSR may pre-insert transmission depth before LightPass for Hi-Z. In that
607
+ // case the refraction scene-color copy needs an isolated depth buffer so
608
+ // transmission surfaces do not occlude the background they sample.
609
+ const copyFramebufferHandle = !options.needsTransmissionDepthForSSR ? builder.createFramebuffer({
610
+ label: 'SceneColorCopyFramebuffer',
611
+ width: ctx.renderWidth,
612
+ height: ctx.renderHeight,
613
+ colorAttachments: copyHandle,
614
+ depthAttachment: renderDepthAttachment,
615
+ ignoreDepthStencil: false
616
+ }) : undefined;
617
+ builder.setExecute((rgCtx)=>{
618
+ renderSceneColorGrab(frame, rgCtx, copyHandle, copyFramebufferHandle);
619
+ });
620
+ return {
621
+ copyHandle,
622
+ copyFramebufferHandle
623
+ };
624
+ });
625
+ }
527
626
  const lightPassResult = graph.addPass('LightPass', (builder)=>{
528
627
  builder.read(depthHandle);
529
628
  builder.read(depthPassResult.depthFramebufferHandle);
629
+ if (shadowMaskHandle) {
630
+ builder.read(shadowMaskHandle);
631
+ }
530
632
  if (preLightTransmissionDepthToken) {
531
633
  builder.read(preLightTransmissionDepthToken);
532
634
  }
@@ -536,18 +638,18 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
536
638
  for (const binding of lightHistoryReadBindings){
537
639
  builder.read(binding.handle);
538
640
  }
539
- // Create scene color texture (intermediate render target)
540
- const sceneColorHandle = builder.createTexture({
641
+ // Scene color: in final-framebuffer-as-intermediate mode the scene is
642
+ // physically rendered into the final framebuffer, so declare the
643
+ // backbuffer write — the graph sees the real data flow and no keep-alive
644
+ // reads are needed downstream. Otherwise render into a graph texture.
645
+ const sceneColorHandle = useFinalFramebufferAsIntermediate ? builder.write(backbuffer) : builder.createTexture({
541
646
  format: ctx.colorFormat,
542
647
  label: 'sceneColor'
543
648
  });
544
- // Create optional sceneColorCopy for transmission/refraction materials
545
- let sceneColorCopyHandle;
546
- if (options.needSceneColor) {
547
- sceneColorCopyHandle = builder.createTexture({
548
- format: ctx.colorFormat,
549
- label: 'sceneColorCopy'
550
- });
649
+ // Transmission/refraction background produced by the SceneColorGrab pass
650
+ const sceneColorCopyHandle = grabResult?.copyHandle;
651
+ if (sceneColorCopyHandle) {
652
+ builder.read(sceneColorCopyHandle);
551
653
  }
552
654
  if (sssProfileResult) {
553
655
  builder.read(sssProfileResult.profileHandle);
@@ -557,6 +659,17 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
557
659
  }
558
660
  }
559
661
  const includeSSRSurfaceMRT = !!options.ssr;
662
+ // SSR glossy-surface MRT outputs (roughness + world normal) are graph
663
+ // textures owned by this pass; effects reach them through the blackboard
664
+ // handles (or the ctx fields resolved below during execution).
665
+ const ssrRoughnessHandle = includeSSRSurfaceMRT ? builder.createTexture({
666
+ format: getSurfaceTextureFormat(ctx),
667
+ label: 'ssrRoughness'
668
+ }) : undefined;
669
+ const ssrNormalHandle = includeSSRSurfaceMRT ? builder.createTexture({
670
+ format: getSurfaceTextureFormat(ctx),
671
+ label: 'ssrNormal'
672
+ }) : undefined;
560
673
  const writeSSSDiffuse = options.sss && shouldStoreSSSDiffuse(ctx);
561
674
  let writeSSSTransmission = options.sss && shouldStoreSSSTransmission(ctx);
562
675
  if (writeSSSDiffuse && writeSSSTransmission && includeSSRSurfaceMRT && getSSSLightingTextureFormat(ctx, 2, includeSSRSurfaceMRT) !== ctx.colorFormat) {
@@ -584,20 +697,13 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
584
697
  colorAttachments: sceneColorHandle,
585
698
  depthAttachment: renderDepthAttachment
586
699
  });
587
- // SSR may pre-insert transmission depth before LightPass for Hi-Z. In that case the
588
- // refraction scene-color copy needs an isolated depth buffer so transmission surfaces
589
- // do not occlude the background color they are about to sample.
590
- const sceneColorCopyFramebufferHandle = sceneColorCopyHandle && !options.needsTransmissionDepthForSSR ? builder.createFramebuffer({
591
- label: 'SceneColorCopyFramebuffer',
592
- width: ctx.renderWidth,
593
- height: ctx.renderHeight,
594
- colorAttachments: sceneColorCopyHandle,
595
- depthAttachment: renderDepthAttachment,
596
- ignoreDepthStencil: false
597
- }) : undefined;
598
700
  builder.setExecute((rgCtx)=>{
599
701
  const sceneColorTex = rgCtx.getTexture(sceneColorHandle);
600
702
  const sceneColorCopyTex = sceneColorCopyHandle ? rgCtx.getTexture(sceneColorCopyHandle) : null;
703
+ // Resolve MRT products into the DrawContext bridge fields that scene
704
+ // rendering and apply()-based effects still read.
705
+ ctx.SSRRoughnessTexture = ssrRoughnessHandle ? rgCtx.getTexture(ssrRoughnessHandle) : null;
706
+ ctx.SSRNormalTexture = ssrNormalHandle ? rgCtx.getTexture(ssrNormalHandle) : null;
601
707
  if (sssProfileResult) {
602
708
  ctx.SSSProfileTexture = rgCtx.getTexture(sssProfileResult.profileHandle);
603
709
  ctx.SSSParamTexture = rgCtx.getTexture(sssProfileResult.paramHandle);
@@ -608,7 +714,7 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
608
714
  ctx.SSSDiffuseTexture = sssDiffuseHandle ? rgCtx.getTexture(sssDiffuseHandle) : null;
609
715
  ctx.SSSTransmissionTexture = sssTransmissionHandle ? rgCtx.getTexture(sssTransmissionHandle) : null;
610
716
  ctx.SkinSSSTexture = skinSSSHandle ? rgCtx.getTexture(skinSSSHandle) : null;
611
- const renderLightPass = ()=>renderMainLightPass(frame, sceneColorTex, sceneColorCopyTex, rgCtx, sceneColorFramebufferHandle, sceneColorCopyFramebufferHandle);
717
+ const renderLightPass = ()=>renderOpaqueScenePass(frame, sceneColorTex, sceneColorCopyTex, rgCtx, sceneColorFramebufferHandle);
612
718
  if (historyManager && lightHistoryReadBindings.length > 0) {
613
719
  historyManager.beginReadScope(lightHistoryReadBindings.map((binding)=>({
614
720
  name: binding.name,
@@ -626,66 +732,247 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
626
732
  return {
627
733
  sceneColorHandle,
628
734
  sceneColorCopyHandle,
629
- sceneColorFramebufferHandle
735
+ sceneColorFramebufferHandle,
736
+ ssrRoughnessHandle,
737
+ ssrNormalHandle,
738
+ sssDiffuseHandle,
739
+ sssTransmissionHandle,
740
+ skinSSSHandle
630
741
  };
631
742
  });
632
- const sceneColorHandle = lightPassResult.sceneColorHandle;
633
- // 8. Transmission depth pass (optional)
634
- let transmissionDepthToken;
635
- if (options.needSceneColor && !options.needsTransmissionDepthForSSR) {
636
- transmissionDepthToken = graph.addPass('TransmissionDepth', (builder)=>{
637
- builder.read(sceneColorHandle);
638
- builder.read(depthPassResult.depthFramebufferHandle);
639
- const done = builder.createToken('TransmissionDepthDone');
640
- builder.sideEffect();
641
- builder.setExecute((rgCtx)=>{
642
- renderTransmissionDepthPass(frame, rgCtx);
643
- });
644
- return done;
645
- });
743
+ // Register the LightPass MRT products so effects can look them up by name.
744
+ if (lightPassResult.ssrRoughnessHandle) {
745
+ blackboard.set(FrameResources.SSRRoughness, lightPassResult.ssrRoughnessHandle);
746
+ }
747
+ if (lightPassResult.ssrNormalHandle) {
748
+ blackboard.set(FrameResources.SSRNormal, lightPassResult.ssrNormalHandle);
749
+ } else if (sssProfileResult?.normalHandle) {
750
+ blackboard.set(FrameResources.SSRNormal, sssProfileResult.normalHandle);
751
+ }
752
+ if (lightPassResult.sssDiffuseHandle) {
753
+ blackboard.set(FrameResources.SSSDiffuse, lightPassResult.sssDiffuseHandle);
754
+ }
755
+ if (lightPassResult.sssTransmissionHandle) {
756
+ blackboard.set(FrameResources.SSSTransmission, lightPassResult.sssTransmissionHandle);
757
+ }
758
+ if (lightPassResult.skinSSSHandle) {
759
+ blackboard.set(FrameResources.SkinSSS, lightPassResult.skinSSSHandle);
646
760
  }
647
- // 9. Post effects + final composite
648
- const presentedBackbuffer = graph.addPass('Composite', (builder)=>{
649
- builder.read(sceneColorHandle);
761
+ // 7d. Opaque-layer post effects (SAO/SSR/SSS/SkinSSS). They read the opaque
762
+ // scene color and must complete before transparent geometry renders on top
763
+ // of their output. Never direct-write: the chain output becomes the
764
+ // transparent pass's render target. In final-framebuffer-as-intermediate
765
+ // mode the scene color handle is the LightPass's backbuffer write version,
766
+ // so the data flow is real either way — no keep-alive reads needed.
767
+ const opaqueChainInput = lightPassResult.sceneColorHandle;
768
+ // Data dependencies for every effect pass: frame textures the effects sample
769
+ // through DrawContext fields (linear depth, HiZ, scene color copy, SSS MRT
770
+ // outputs) rather than through declared require* hooks.
771
+ const opaqueChainDeps = [
772
+ depthHandle
773
+ ];
774
+ if (hiZHandle) {
775
+ opaqueChainDeps.push(hiZHandle);
776
+ }
777
+ if (lightPassResult.sceneColorFramebufferHandle) {
778
+ opaqueChainDeps.push(lightPassResult.sceneColorFramebufferHandle);
779
+ }
780
+ if (grabResult) {
781
+ opaqueChainDeps.push(grabResult.copyHandle);
782
+ }
783
+ if (sssProfileResult) {
784
+ opaqueChainDeps.push(sssProfileResult.profileHandle, sssProfileResult.paramHandle);
785
+ if (sssProfileResult.normalHandle) {
786
+ opaqueChainDeps.push(sssProfileResult.normalHandle);
787
+ }
788
+ }
789
+ for (const handle of [
790
+ lightPassResult.ssrRoughnessHandle,
791
+ lightPassResult.ssrNormalHandle,
792
+ lightPassResult.sssDiffuseHandle,
793
+ lightPassResult.sssTransmissionHandle,
794
+ lightPassResult.skinSSSHandle
795
+ ]){
796
+ if (handle) {
797
+ opaqueChainDeps.push(handle);
798
+ }
799
+ }
800
+ const opaqueChainResult = ctx.compositor ? ctx.compositor.buildLayer({
801
+ graph,
802
+ ctx,
803
+ layer: PostEffectLayer.opaque,
804
+ blackboard,
805
+ input: opaqueChainInput,
806
+ finalOutput: null,
807
+ sceneDepthAttachment: renderDepthAttachment,
808
+ dependencies: opaqueChainDeps,
809
+ historyReads: lightHistoryReadBindings,
810
+ history: historyManager
811
+ }) : {
812
+ color: opaqueChainInput,
813
+ wroteFinal: false
814
+ };
815
+ const opaqueChainRan = opaqueChainResult.color !== opaqueChainInput;
816
+ // 7e. Transparent scene geometry (transmission/transparent lists + OIT).
817
+ // Renders on top of the opaque-chain output; graph-wise an in-place write
818
+ // producing a new version of the current scene color.
819
+ const sceneColorHandle = graph.addPass('TransparentPass', (builder)=>{
650
820
  builder.read(depthHandle);
821
+ builder.read(depthPassResult.depthFramebufferHandle);
651
822
  if (hiZHandle) {
823
+ // Transparent-phase materials may ray-march HiZ (e.g. water SSR)
652
824
  builder.read(hiZHandle);
653
825
  }
654
- if (motionVectorHandle) {
655
- builder.read(motionVectorHandle);
656
- }
657
826
  if (lightPassResult.sceneColorFramebufferHandle) {
658
827
  builder.read(lightPassResult.sceneColorFramebufferHandle);
659
828
  }
660
- if (transmissionDepthToken) {
661
- builder.read(transmissionDepthToken);
829
+ if (lightPassResult.sceneColorCopyHandle) {
830
+ builder.read(lightPassResult.sceneColorCopyHandle);
662
831
  }
663
- for (const binding of compositeHistoryReadBindings){
664
- builder.read(binding.handle);
665
- }
666
- const outputBackbuffer = builder.write(backbuffer);
667
- const taaHistoryColorHandle = useFinalFramebufferAsIntermediate ? outputBackbuffer : sceneColorHandle;
832
+ builder.read(opaqueChainResult.color);
833
+ const out = builder.write(opaqueChainResult.color);
668
834
  builder.setExecute((rgCtx)=>{
669
- const renderAndCommitComposite = ()=>{
670
- renderComposite(frame);
671
- queueTAAHistoryCommit(frame, rgCtx, historyManager, taaHistoryColorHandle, motionVectorHandle);
835
+ renderTransparentScenePass(frame, rgCtx, opaqueChainRan ? opaqueChainResult.color : null, lightPassResult.sceneColorFramebufferHandle);
836
+ });
837
+ return out;
838
+ });
839
+ blackboard.set(FrameResources.SceneColor, sceneColorHandle);
840
+ if (lightPassResult.sceneColorCopyHandle) {
841
+ blackboard.set(FrameResources.SceneColorCopy, lightPassResult.sceneColorCopyHandle);
842
+ }
843
+ if (sssProfileResult) {
844
+ blackboard.set(FrameResources.SSSProfile, sssProfileResult.profileHandle);
845
+ blackboard.set(FrameResources.SSSParam, sssProfileResult.paramHandle);
846
+ }
847
+ // 8. Post effect chains + transmission depth.
848
+ //
849
+ // Chain input: the TransparentPass output version — the authoritative scene
850
+ // color regardless of whether it physically lives in a texture or in the
851
+ // backbuffer (final framebuffer used as intermediate, no opaque effects).
852
+ const chainInput = sceneColorHandle;
853
+ // When the scene color still physically resides in the final framebuffer,
854
+ // the Present blit must be skipped if no effect moved it to a texture.
855
+ // (Opaque-layer effects disable final-as-intermediate mode, so the scene
856
+ // color is backbuffer-resident whenever that mode is active.)
857
+ const backbufferResidentHandle = useFinalFramebufferAsIntermediate ? sceneColorHandle : null;
858
+ // No extra chain dependencies: the effect chains link to the scene color
859
+ // through their inputs, and per-effect texture needs are declared via the
860
+ // require* hooks in AbstractPostEffect.setup().
861
+ const chainDependencies = [];
862
+ const finalOutput = {
863
+ handle: backbuffer,
864
+ isScreen: !ctx.finalFramebuffer
865
+ };
866
+ const endLayerHasEffects = !!ctx.compositor?.layerHasEnabledEffect(PostEffectLayer.end);
867
+ // 8a. Transparent-layer effects (bloom, tonemap, FXAA, ...). They run right
868
+ // after the light pass and must sample the pre-transmission linear depth, so
869
+ // they carry no transmissionDepthToken dependency; TransmissionDepth is
870
+ // instead ordered after this chain (see 8b).
871
+ const transparentChainResult = ctx.compositor ? ctx.compositor.buildLayer({
872
+ graph,
873
+ ctx,
874
+ layer: PostEffectLayer.transparent,
875
+ blackboard,
876
+ input: chainInput,
877
+ finalOutput: endLayerHasEffects ? null : finalOutput,
878
+ inputResidesInFinalTarget: !!backbufferResidentHandle,
879
+ sceneDepthAttachment: renderDepthAttachment,
880
+ dependencies: chainDependencies,
881
+ history: historyManager
882
+ }) : {
883
+ color: chainInput,
884
+ wroteFinal: false
885
+ };
886
+ // 8b. Transmission depth pass (optional). Mutates the linear depth texture;
887
+ // the mutation is modeled as a graph write: the WAR hazard orders this pass
888
+ // after every pre-transmission depth reader (the transparent-layer chain),
889
+ // and re-registering the post-write version in the blackboard gives
890
+ // end-layer effects (TAA) a real data dependency on the transmission depth.
891
+ let transmissionDepthToken;
892
+ if (options.needSceneColor && !options.needsTransmissionDepthForSSR) {
893
+ const transmissionDepthResult = graph.addPass('TransmissionDepth', (builder)=>{
894
+ builder.read(sceneColorHandle);
895
+ if (transparentChainResult.color !== sceneColorHandle) {
896
+ builder.read(transparentChainResult.color);
897
+ }
898
+ builder.read(depthHandle);
899
+ builder.read(depthPassResult.depthFramebufferHandle);
900
+ const depthOut = builder.write(depthHandle);
901
+ const done = builder.createToken('TransmissionDepthDone');
902
+ builder.sideEffect();
903
+ builder.setExecute((rgCtx)=>{
904
+ renderTransmissionDepthPass(frame, rgCtx);
905
+ });
906
+ return {
907
+ done,
908
+ depthOut
672
909
  };
673
- if (historyManager && compositeHistoryReadBindings.length > 0) {
674
- historyManager.beginReadScope(compositeHistoryReadBindings.map((binding)=>({
675
- name: binding.name,
676
- texture: rgCtx.getTexture(binding.handle)
677
- })));
678
- try {
679
- renderAndCommitComposite();
680
- } finally{
681
- historyManager.endReadScope();
682
- }
683
- } else {
684
- renderAndCommitComposite();
910
+ });
911
+ transmissionDepthToken = transmissionDepthResult.done;
912
+ depthHandle = transmissionDepthResult.depthOut;
913
+ // The transparent-layer chain above read the pre-transmission version;
914
+ // everything built from here on (end-layer chain) reads this one.
915
+ blackboard.set(FrameResources.LinearDepth, depthHandle);
916
+ }
917
+ // 9. End-layer effects (TAA). Ordered after TransmissionDepth.
918
+ const endChainDependencies = transmissionDepthToken ? [
919
+ ...chainDependencies,
920
+ transmissionDepthToken
921
+ ] : chainDependencies;
922
+ const chainResult = ctx.compositor ? ctx.compositor.buildLayer({
923
+ graph,
924
+ ctx,
925
+ layer: PostEffectLayer.end,
926
+ blackboard,
927
+ input: transparentChainResult.color,
928
+ finalOutput,
929
+ // Still backbuffer-resident if the transparent-layer chain ran no effect
930
+ inputResidesInFinalTarget: !!backbufferResidentHandle && transparentChainResult.color === chainInput,
931
+ sceneDepthAttachment: renderDepthAttachment,
932
+ dependencies: endChainDependencies,
933
+ history: historyManager
934
+ }) : {
935
+ color: transparentChainResult.color,
936
+ wroteFinal: false
937
+ };
938
+ const finalWroteFinal = chainResult.wroteFinal || transparentChainResult.wroteFinal;
939
+ // 10. Present + frame cleanup.
940
+ let presentedBackbuffer;
941
+ if (finalWroteFinal) {
942
+ // The last effect wrote the final target directly; only cleanup remains.
943
+ presentedBackbuffer = chainResult.color;
944
+ graph.addPass('FrameCleanup', (builder)=>{
945
+ builder.read(presentedBackbuffer);
946
+ for (const dep of endChainDependencies){
947
+ builder.read(dep);
685
948
  }
949
+ builder.sideEffect();
950
+ builder.setExecute(()=>{
951
+ finishFrame(frame);
952
+ });
686
953
  });
687
- return outputBackbuffer;
688
- });
954
+ } else {
955
+ presentedBackbuffer = graph.addPass('Present', (builder)=>{
956
+ builder.read(chainResult.color);
957
+ for (const dep of endChainDependencies){
958
+ builder.read(dep);
959
+ }
960
+ const outputBackbuffer = builder.write(backbuffer);
961
+ // Skip the blit when the chain output already lives in the final target
962
+ // (final framebuffer used as intermediate and no end-layer effect ran).
963
+ const needsBlit = chainResult.color !== backbufferResidentHandle;
964
+ builder.setExecute((rgCtx)=>{
965
+ const sourceTex = needsBlit ? rgCtx.getTexture(chainResult.color) : null;
966
+ if (sourceTex) {
967
+ const blitter = new CopyBlitter();
968
+ blitter.srgbOut = !ctx.finalFramebuffer;
969
+ blitter.blit(sourceTex, ctx.finalFramebuffer ?? null, fetchSampler('clamp_nearest_nomip'));
970
+ }
971
+ finishFrame(frame);
972
+ });
973
+ return outputBackbuffer;
974
+ });
975
+ }
689
976
  return {
690
977
  backbuffer: presentedBackbuffer,
691
978
  frame
@@ -766,12 +1053,9 @@ function renderForwardSSSProfile(frame, profileFramebuffer, profileTexture, para
766
1053
  }
767
1054
  }
768
1055
  function releaseIntermediateFramebuffer(frame) {
769
- const { ctx } = frame;
770
- if (frame.intermediateDeviceStatePushed) {
771
- ctx.device.popDeviceStates();
772
- frame.intermediateDeviceStatePushed = false;
773
- }
774
- ctx.intermediateFramebuffer = null;
1056
+ // Device state is contained within each pass now (LightPass/TransparentPass
1057
+ // push/pop their own framebuffer bindings); only the context field remains.
1058
+ frame.ctx.intermediateFramebuffer = null;
775
1059
  }
776
1060
  function releaseDepthFramebuffer(frame) {
777
1061
  frame.depthFramebuffer = null;
@@ -965,9 +1249,58 @@ function blitToCurrentColorAttachment(ctx, source) {
965
1249
  const destination = framebuffer?.getColorAttachment(0) ?? null;
966
1250
  new CopyBlitter().blit(source, destination, fetchSampler('clamp_nearest_nomip'));
967
1251
  }
968
- /** @internal */ function renderMainLightPass(frame, sceneColorTex, sceneColorCopyTex, rgCtx, sceneColorFramebufferHandle, sceneColorCopyFramebufferHandle) {
1252
+ /**
1253
+ * Renders the full scene (no transmission) into the scene-color copy texture
1254
+ * used as refraction background. Runs as its own graph pass before LightPass.
1255
+ * @internal
1256
+ */ function renderSceneColorGrab(frame, rgCtx, copyHandle, copyFramebufferHandle) {
969
1257
  const { ctx, renderQueue } = frame;
970
1258
  const device = ctx.device;
1259
+ const depthTex = frame.depthFramebuffer?.getDepthAttachment();
1260
+ const copyTex = rgCtx.getTexture(copyHandle);
1261
+ const compositor = ctx.compositor;
1262
+ ctx.compositor = null;
1263
+ const isolateSceneColorDepth = frame.options.needsTransmissionDepthForSSR;
1264
+ const savedDepthPrepassAttachment = ctx.depthPrepassAttachment;
1265
+ const savedMaterialFlags = ctx.materialFlags;
1266
+ // MRT store flags never apply to the background copy
1267
+ const sceneColorMaterialFlags = ctx.materialFlags & ~SURFACE_MRT_FLAGS;
1268
+ const sceneColorFramebuffer = copyFramebufferHandle ? rgCtx.getFramebuffer(copyFramebufferHandle) : rgCtx.createFramebuffer({
1269
+ width: copyTex.width,
1270
+ height: copyTex.height,
1271
+ colorAttachments: copyTex,
1272
+ depthAttachment: isolateSceneColorDepth ? ctx.depthFormat : depthTex,
1273
+ ignoreDepthStencil: false
1274
+ });
1275
+ let sceneColorStatePushed = false;
1276
+ try {
1277
+ device.pushDeviceStates();
1278
+ sceneColorStatePushed = true;
1279
+ device.setFramebuffer(sceneColorFramebuffer);
1280
+ _scenePass.transmission = false;
1281
+ if (isolateSceneColorDepth) {
1282
+ ctx.depthPrepassAttachment = undefined;
1283
+ _scenePass.clearDepth = 1;
1284
+ _scenePass.clearStencil = 0;
1285
+ } else {
1286
+ _scenePass.clearDepth = depthTex ? null : 1;
1287
+ _scenePass.clearStencil = depthTex ? null : 0;
1288
+ }
1289
+ ctx.materialFlags = sceneColorMaterialFlags;
1290
+ _scenePass.render(ctx, null, null, renderQueue);
1291
+ } finally{
1292
+ ctx.materialFlags = savedMaterialFlags;
1293
+ if (isolateSceneColorDepth) {
1294
+ ctx.depthPrepassAttachment = savedDepthPrepassAttachment;
1295
+ }
1296
+ if (sceneColorStatePushed) {
1297
+ device.popDeviceStates();
1298
+ }
1299
+ ctx.compositor = compositor;
1300
+ }
1301
+ }
1302
+ /** @internal */ function renderOpaqueScenePass(frame, sceneColorTex, sceneColorCopyTex, rgCtx, sceneColorFramebufferHandle) {
1303
+ const { ctx, renderQueue } = frame;
971
1304
  // Use RenderGraph-allocated scene color texture
972
1305
  const depthTex = frame.depthFramebuffer?.getDepthAttachment();
973
1306
  ctx.materialFlags &= ~(MaterialVaryingFlags.SSR_STORE_ROUGHNESS | MaterialVaryingFlags.SSS_STORE_PROFILE | MaterialVaryingFlags.SSS_STORE_DIFFUSE | MaterialVaryingFlags.SSS_STORE_NORMAL | MaterialVaryingFlags.SSS_STORE_TRANSMISSION | MaterialVaryingFlags.SKIN_SSS_STORE);
@@ -985,10 +1318,15 @@ function blitToCurrentColorAttachment(ctx, source) {
985
1318
  if (ctx.SkinSSSTexture) {
986
1319
  ctx.materialFlags |= MaterialVaryingFlags.SKIN_SSS_STORE;
987
1320
  }
988
- if (depthTex === ctx.finalFramebuffer?.getDepthAttachment()) {
989
- ctx.intermediateFramebuffer = ctx.finalFramebuffer;
990
- } else if (sceneColorFramebufferHandle && !hasSurfaceMRT(ctx)) {
1321
+ // The graph scene color framebuffer takes priority: it is absent only when
1322
+ // the graph was built in final-framebuffer-as-intermediate mode (external
1323
+ // depth shared with the final framebuffer AND no opaque-layer effects).
1324
+ // Checking the shared depth first would wrongly route the MRT/opaque-effect
1325
+ // case into the single-color final framebuffer.
1326
+ if (sceneColorFramebufferHandle && !hasSurfaceMRT(ctx)) {
991
1327
  ctx.intermediateFramebuffer = rgCtx.getFramebuffer(sceneColorFramebufferHandle);
1328
+ } else if (!sceneColorFramebufferHandle && depthTex === ctx.finalFramebuffer?.getDepthAttachment()) {
1329
+ ctx.intermediateFramebuffer = ctx.finalFramebuffer;
992
1330
  } else {
993
1331
  ctx.intermediateFramebuffer = rgCtx.createFramebuffer({
994
1332
  width: sceneColorTex.width,
@@ -997,104 +1335,122 @@ function blitToCurrentColorAttachment(ctx, source) {
997
1335
  depthAttachment: depthTex
998
1336
  });
999
1337
  }
1000
- if (ctx.intermediateFramebuffer && ctx.intermediateFramebuffer !== ctx.finalFramebuffer) {
1001
- device.pushDeviceStates();
1002
- frame.intermediateDeviceStatePushed = true;
1003
- device.setFramebuffer(ctx.intermediateFramebuffer);
1338
+ // The scene target is bound explicitly and the device state is restored at
1339
+ // the end of this pass: graph passes never communicate through leftover
1340
+ // device state. TransparentPass re-binds the same target (or the opaque
1341
+ // chain output) explicitly.
1342
+ ctx.device.pushDeviceStates();
1343
+ try {
1344
+ // setFramebuffer() no-ops when the target is unchanged, so reset the
1345
+ // viewport/scissor explicitly to cover that case.
1346
+ ctx.device.setFramebuffer(ctx.intermediateFramebuffer);
1347
+ ctx.device.setViewport(null);
1348
+ ctx.device.setScissor(null);
1349
+ _scenePass.transmission = false;
1350
+ _scenePass.clearDepth = depthTex ? null : 1;
1351
+ _scenePass.clearStencil = depthTex ? null : 0;
1352
+ if (renderQueue.needSceneColor() && sceneColorCopyTex) {
1353
+ // Background copy was produced by the SceneColorGrab pass; seed the main
1354
+ // color attachment with it and render only transmission/transparent on top.
1355
+ ctx.sceneColorTexture = sceneColorCopyTex;
1356
+ blitToCurrentColorAttachment(ctx, ctx.sceneColorTexture);
1357
+ if (hasSurfaceMRT(ctx)) {
1358
+ // The background copy carries no surface MRT attachments, so opaque
1359
+ // geometry exists only there without roughness/normal (and SSS
1360
+ // lighting) data. Re-render the opaque lists into the MRT scene
1361
+ // target: early-z against the prepass depth keeps this cheap, and the
1362
+ // color output matches the blitted copy on opaque pixels while the
1363
+ // MRT attachments receive the surface data SSR/SSS require.
1364
+ _scenePass.clearColor = null;
1365
+ _scenePass.clearDepth = null;
1366
+ _scenePass.clearStencil = null;
1367
+ _scenePass.renderOpaque = true;
1368
+ _scenePass.renderTransparent = false;
1369
+ _scenePass.render(ctx, null, null, renderQueue);
1370
+ }
1371
+ _scenePass.transmission = true;
1372
+ _scenePass.clearColor = null;
1373
+ _scenePass.clearDepth = null;
1374
+ _scenePass.clearStencil = null;
1375
+ }
1376
+ _scenePass.renderOpaque = true;
1377
+ _scenePass.renderTransparent = false;
1378
+ _scenePass.render(ctx, null, null, renderQueue);
1379
+ _scenePass.renderTransparent = true;
1380
+ } finally{
1381
+ ctx.device.popDeviceStates();
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Renders the transmission/transparent geometry lists (including OIT) on top
1386
+ * of the opaque result. The target framebuffer is always bound explicitly:
1387
+ * the opaque chain output when opaque-layer effects ran, otherwise the same
1388
+ * scene target the light pass rendered into. OIT implementations composite
1389
+ * into the currently bound framebuffer, which this pass guarantees.
1390
+ * @internal
1391
+ */ function renderTransparentScenePass(frame, rgCtx, opaqueChainOutput, sceneColorFramebufferHandle) {
1392
+ const { ctx, renderQueue } = frame;
1393
+ const device = ctx.device;
1394
+ let framebuffer;
1395
+ if (opaqueChainOutput) {
1396
+ // Opaque-layer effects redirected the scene color into their chain output;
1397
+ // transparent geometry renders on top of it with the scene depth attached.
1398
+ const chainTex = rgCtx.getTexture(opaqueChainOutput);
1399
+ const depthTex = frame.depthFramebuffer?.getDepthAttachment();
1400
+ framebuffer = rgCtx.createFramebuffer({
1401
+ width: chainTex.width,
1402
+ height: chainTex.height,
1403
+ colorAttachments: chainTex,
1404
+ depthAttachment: depthTex
1405
+ });
1406
+ // The chain output is single-color: surface MRT stores no longer apply.
1407
+ ctx.materialFlags &= ~SURFACE_MRT_FLAGS;
1408
+ } else if (sceneColorFramebufferHandle) {
1409
+ // No opaque-layer effect ran (thus no surface MRT either): continue in the
1410
+ // graph scene color framebuffer the light pass rendered into.
1411
+ framebuffer = rgCtx.getFramebuffer(sceneColorFramebufferHandle);
1004
1412
  } else {
1413
+ // Final framebuffer used as scene intermediate.
1414
+ framebuffer = ctx.finalFramebuffer;
1415
+ }
1416
+ device.pushDeviceStates();
1417
+ try {
1418
+ // setFramebuffer() no-ops when the target is unchanged, so reset the
1419
+ // viewport/scissor explicitly to cover that case.
1420
+ device.setFramebuffer(framebuffer);
1005
1421
  device.setViewport(null);
1006
1422
  device.setScissor(null);
1007
- }
1008
- _scenePass.transmission = false;
1009
- _scenePass.clearDepth = depthTex ? null : 1;
1010
- _scenePass.clearStencil = depthTex ? null : 0;
1011
- ctx.compositor?.begin(ctx);
1012
- if (renderQueue.needSceneColor() && sceneColorCopyTex) {
1013
- const compositor = ctx.compositor;
1014
- ctx.compositor = null;
1015
- const isolateSceneColorDepth = frame.options.needsTransmissionDepthForSSR;
1016
- const savedDepthPrepassAttachment = ctx.depthPrepassAttachment;
1017
- const savedClearDepth = _scenePass.clearDepth;
1018
- const savedClearStencil = _scenePass.clearStencil;
1019
- const savedMaterialFlags = ctx.materialFlags;
1020
- // Use RenderGraph-allocated sceneColorCopy texture
1021
- const sceneColorMaterialFlags = ctx.materialFlags & ~SURFACE_MRT_FLAGS;
1022
- const sceneColorFramebuffer = sceneColorCopyFramebufferHandle ? rgCtx.getFramebuffer(sceneColorCopyFramebufferHandle) : rgCtx.createFramebuffer({
1023
- width: sceneColorCopyTex.width,
1024
- height: sceneColorCopyTex.height,
1025
- colorAttachments: sceneColorCopyTex,
1026
- depthAttachment: isolateSceneColorDepth ? ctx.depthFormat : depthTex,
1027
- ignoreDepthStencil: false
1028
- });
1029
- let sceneColorStatePushed = false;
1423
+ // _scenePass.transmission carries over from the opaque phase (true when the
1424
+ // scene color copy seeded the background, false otherwise). Never clear:
1425
+ // the opaque result is already in the target.
1426
+ _scenePass.clearColor = null;
1427
+ _scenePass.clearDepth = null;
1428
+ _scenePass.clearStencil = null;
1429
+ _scenePass.renderOpaque = false;
1430
+ _scenePass.renderTransparent = true;
1030
1431
  try {
1031
- device.pushDeviceStates();
1032
- sceneColorStatePushed = true;
1033
- device.setFramebuffer(sceneColorFramebuffer);
1034
- _scenePass.transmission = false;
1035
- if (isolateSceneColorDepth) {
1036
- ctx.depthPrepassAttachment = undefined;
1037
- _scenePass.clearDepth = 1;
1038
- _scenePass.clearStencil = 0;
1039
- }
1040
- ctx.materialFlags = sceneColorMaterialFlags;
1041
1432
  _scenePass.render(ctx, null, null, renderQueue);
1042
1433
  } finally{
1043
- ctx.materialFlags = savedMaterialFlags;
1044
- if (isolateSceneColorDepth) {
1045
- ctx.depthPrepassAttachment = savedDepthPrepassAttachment;
1046
- _scenePass.clearDepth = savedClearDepth;
1047
- _scenePass.clearStencil = savedClearStencil;
1048
- }
1049
- if (sceneColorStatePushed) {
1050
- device.popDeviceStates();
1051
- }
1052
- ctx.compositor = compositor;
1434
+ _scenePass.renderOpaque = true;
1053
1435
  }
1054
- ctx.sceneColorTexture = sceneColorCopyTex;
1055
- blitToCurrentColorAttachment(ctx, ctx.sceneColorTexture);
1056
- _scenePass.transmission = true;
1057
- _scenePass.clearColor = null;
1058
- _scenePass.clearDepth = null;
1059
- _scenePass.clearStencil = null;
1436
+ } finally{
1437
+ device.popDeviceStates();
1060
1438
  }
1061
- _scenePass.render(ctx, null, null, renderQueue);
1062
1439
  }
1063
1440
  /** @internal */ function renderTransmissionDepthPass(frame, rgCtx) {
1064
1441
  renderSceneDepth(frame, frame.depthFramebuffer, rgCtx);
1065
1442
  }
1066
- function queueTAAHistoryCommit(frame, rgCtx, historyManager, colorHandle, motionVectorHandle) {
1067
- const { ctx } = frame;
1068
- if (!historyManager?.frameActive || !ctx.camera.TAA || !frame.options.motionVectors || !motionVectorHandle) {
1069
- return;
1070
- }
1071
- const colorTexture = rgCtx.getTexture(colorHandle);
1072
- const colorSize = {
1073
- width: colorTexture.width,
1074
- height: colorTexture.height
1075
- };
1076
- historyManager.queueRetainedCommit(RGHistoryResources.TAA_COLOR, {
1077
- format: colorTexture.format,
1078
- sizeMode: 'absolute',
1079
- width: colorTexture.width,
1080
- height: colorTexture.height
1081
- }, colorSize, colorTexture);
1082
- const motionVectorTexture = rgCtx.getTexture(motionVectorHandle);
1083
- const motionVectorSize = {
1084
- width: motionVectorTexture.width,
1085
- height: motionVectorTexture.height
1086
- };
1087
- historyManager.queueRetainedCommit(RGHistoryResources.TAA_MOTION_VECTOR, {
1088
- format: motionVectorTexture.format,
1089
- sizeMode: 'absolute',
1090
- width: motionVectorTexture.width,
1091
- height: motionVectorTexture.height
1092
- }, motionVectorSize, motionVectorTexture);
1093
- }
1094
- /** @internal */ function renderComposite(frame) {
1443
+ /**
1444
+ * Frame-tail housekeeping shared by the Present and FrameCleanup passes.
1445
+ *
1446
+ * The end-layer post effect chain and the final blit are graph passes now
1447
+ * (see buildForwardPlusGraphInternal step 9/10); this only releases per-frame
1448
+ * state. The compositor's scene ping-pong is flushed back to the intermediate
1449
+ * framebuffer by compositor.end() at the tail of the light pass.
1450
+ *
1451
+ * @internal
1452
+ */ function finishFrame(frame) {
1095
1453
  const { ctx } = frame;
1096
- ctx.compositor?.drawPostEffects(ctx, PostEffectLayer.end, ctx.linearDepthTexture);
1097
- ctx.compositor?.end(ctx);
1098
1454
  disposeRenderQueue(frame);
1099
1455
  ctx.materialFlags &= ~MaterialVaryingFlags.SSR_STORE_ROUGHNESS;
1100
1456
  ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_PROFILE;
@@ -1102,12 +1458,6 @@ function queueTAAHistoryCommit(frame, rgCtx, historyManager, colorHandle, motion
1102
1458
  ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_NORMAL;
1103
1459
  ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_TRANSMISSION;
1104
1460
  ctx.materialFlags &= ~MaterialVaryingFlags.SKIN_SSS_STORE;
1105
- if (ctx.intermediateFramebuffer && ctx.intermediateFramebuffer !== ctx.finalFramebuffer) {
1106
- const blitter = new CopyBlitter();
1107
- blitter.srgbOut = !ctx.finalFramebuffer;
1108
- const srcTex = ctx.intermediateFramebuffer.getColorAttachments()[0];
1109
- blitter.blit(srcTex, ctx.finalFramebuffer ?? null, fetchSampler('clamp_nearest_nomip'));
1110
- }
1111
1461
  releaseIntermediateFramebuffer(frame);
1112
1462
  releaseDepthFramebuffer(frame);
1113
1463
  releaseClusteredLight(frame);