@zephyr3d/scene 0.9.21 → 0.9.23

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 (238) hide show
  1. package/dist/animation/joint_dynamics/collision.js +1 -7
  2. package/dist/animation/joint_dynamics/collision.js.map +1 -1
  3. package/dist/animation/joint_dynamics/constraints.js +2 -17
  4. package/dist/animation/joint_dynamics/constraints.js.map +1 -1
  5. package/dist/animation/joint_dynamics/controller.js +2 -20
  6. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  7. package/dist/animation/joint_dynamics/joint_dynamics_system.js +1 -3
  8. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  9. package/dist/animation/joint_dynamics/solver.js +14 -60
  10. package/dist/animation/joint_dynamics/solver.js.map +1 -1
  11. package/dist/animation/joint_dynamics/types.js +3 -3
  12. package/dist/animation/joint_dynamics/types.js.map +1 -1
  13. package/dist/app/app.js +23 -0
  14. package/dist/app/app.js.map +1 -1
  15. package/dist/app/scriptregistry.js +16 -53
  16. package/dist/app/scriptregistry.js.map +1 -1
  17. package/dist/asset/assetmanager.js +2 -0
  18. package/dist/asset/assetmanager.js.map +1 -1
  19. package/dist/asset/model.js +22 -0
  20. package/dist/asset/model.js.map +1 -1
  21. package/dist/blitter/bilateralblur.js +3 -1
  22. package/dist/blitter/bilateralblur.js.map +1 -1
  23. package/dist/blitter/blitter.js +3 -3
  24. package/dist/blitter/blitter.js.map +1 -1
  25. package/dist/camera/camera.js +398 -45
  26. package/dist/camera/camera.js.map +1 -1
  27. package/dist/camera/perspectivecamera.js +69 -2
  28. package/dist/camera/perspectivecamera.js.map +1 -1
  29. package/dist/index.d.ts +9100 -8712
  30. package/dist/index.js +9 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/material/blinn.js +1 -1
  33. package/dist/material/blinn.js.map +1 -1
  34. package/dist/material/lambert.js +3 -3
  35. package/dist/material/lambert.js.map +1 -1
  36. package/dist/material/material.js +8 -5
  37. package/dist/material/material.js.map +1 -1
  38. package/dist/material/meshmaterial.js +17 -10
  39. package/dist/material/meshmaterial.js.map +1 -1
  40. package/dist/material/mixins/lightmodel/blinnphong.js +1 -1
  41. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  42. package/dist/material/mixins/lightmodel/lambert.js +1 -1
  43. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  44. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +21 -6
  45. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  46. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +1 -1
  47. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  48. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +1 -1
  49. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  50. package/dist/material/mixins/lit.js +33 -11
  51. package/dist/material/mixins/lit.js.map +1 -1
  52. package/dist/material/mixins/pbr/brdf.js +44 -12
  53. package/dist/material/mixins/pbr/brdf.js.map +1 -1
  54. package/dist/material/mixins/pbr/common.js +149 -14
  55. package/dist/material/mixins/pbr/common.js.map +1 -1
  56. package/dist/material/mtoon.js +7 -4
  57. package/dist/material/mtoon.js.map +1 -1
  58. package/dist/material/pbrblueprint.js +5 -1
  59. package/dist/material/pbrblueprint.js.map +1 -1
  60. package/dist/material/pbrmr.js +1 -1
  61. package/dist/material/pbrmr.js.map +1 -1
  62. package/dist/material/pbrsg.js +1 -1
  63. package/dist/material/pbrsg.js.map +1 -1
  64. package/dist/material/shader/helper.js +186 -11
  65. package/dist/material/shader/helper.js.map +1 -1
  66. package/dist/material/skin.js +2 -2
  67. package/dist/material/skin.js.map +1 -1
  68. package/dist/material/terrain-cm.js +5 -5
  69. package/dist/material/terrain-cm.js.map +1 -1
  70. package/dist/material/water.js +6 -4
  71. package/dist/material/water.js.map +1 -1
  72. package/dist/posteffect/bloom.js +54 -4
  73. package/dist/posteffect/bloom.js.map +1 -1
  74. package/dist/posteffect/compositor.js +41 -0
  75. package/dist/posteffect/compositor.js.map +1 -1
  76. package/dist/posteffect/posteffect.js +51 -0
  77. package/dist/posteffect/posteffect.js.map +1 -1
  78. package/dist/posteffect/sao.js +7 -6
  79. package/dist/posteffect/sao.js.map +1 -1
  80. package/dist/posteffect/skinsss.js +3 -3
  81. package/dist/posteffect/skinsss.js.map +1 -1
  82. package/dist/posteffect/ssgi.js +1196 -0
  83. package/dist/posteffect/ssgi.js.map +1 -0
  84. package/dist/posteffect/ssr.js +83 -45
  85. package/dist/posteffect/ssr.js.map +1 -1
  86. package/dist/posteffect/sss.js +137 -49
  87. package/dist/posteffect/sss.js.map +1 -1
  88. package/dist/posteffect/taa.js +5 -4
  89. package/dist/posteffect/taa.js.map +1 -1
  90. package/dist/posteffect/tonemap.js +15 -3
  91. package/dist/posteffect/tonemap.js.map +1 -1
  92. package/dist/render/abuffer_oit.js +2 -2
  93. package/dist/render/abuffer_oit.js.map +1 -1
  94. package/dist/render/cluster_light.js +12 -6
  95. package/dist/render/cluster_light.js.map +1 -1
  96. package/dist/render/drawable_mixin.js +8 -5
  97. package/dist/render/drawable_mixin.js.map +1 -1
  98. package/dist/render/dualdepthpeeling_oit.js +10 -5
  99. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  100. package/dist/render/envlight.js +298 -5
  101. package/dist/render/envlight.js.map +1 -1
  102. package/dist/render/gpu_picking.js +7 -5
  103. package/dist/render/gpu_picking.js.map +1 -1
  104. package/dist/render/hzb.js +28 -19
  105. package/dist/render/hzb.js.map +1 -1
  106. package/dist/render/lightpass.js +18 -4
  107. package/dist/render/lightpass.js.map +1 -1
  108. package/dist/render/render_queue.js +37 -4
  109. package/dist/render/render_queue.js.map +1 -1
  110. package/dist/render/renderer.js +8 -3
  111. package/dist/render/renderer.js.map +1 -1
  112. package/dist/render/rendergraph/blackboard.js +20 -48
  113. package/dist/render/rendergraph/blackboard.js.map +1 -1
  114. package/dist/render/rendergraph/device_pool_allocator.js +18 -58
  115. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  116. package/dist/render/rendergraph/executor.js +89 -115
  117. package/dist/render/rendergraph/executor.js.map +1 -1
  118. package/dist/render/rendergraph/forward_plus_builder.js +1003 -715
  119. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  120. package/dist/render/rendergraph/frame_graph_context.js +41 -0
  121. package/dist/render/rendergraph/frame_graph_context.js.map +1 -0
  122. package/dist/render/rendergraph/frame_resource_requirements.js +14 -0
  123. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -0
  124. package/dist/render/rendergraph/history_resource_manager.js +74 -140
  125. package/dist/render/rendergraph/history_resource_manager.js.map +1 -1
  126. package/dist/render/rendergraph/history_resources.js +7 -5
  127. package/dist/render/rendergraph/history_resources.js.map +1 -1
  128. package/dist/render/rendergraph/render_context.js +19 -0
  129. package/dist/render/rendergraph/render_context.js.map +1 -0
  130. package/dist/render/rendergraph/render_pipeline.js +315 -0
  131. package/dist/render/rendergraph/render_pipeline.js.map +1 -0
  132. package/dist/render/rendergraph/rendergraph.js +158 -141
  133. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  134. package/dist/render/rendergraph/scene_render_context.js +267 -0
  135. package/dist/render/rendergraph/scene_render_context.js.map +1 -0
  136. package/dist/render/rendergraph/texture_affinity_cache.js +19 -0
  137. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -0
  138. package/dist/render/rendergraph/types.js +19 -31
  139. package/dist/render/rendergraph/types.js.map +1 -1
  140. package/dist/render/renderpass.js +3 -3
  141. package/dist/render/renderpass.js.map +1 -1
  142. package/dist/render/shadow_mask_pass.js +10 -6
  143. package/dist/render/shadow_mask_pass.js.map +1 -1
  144. package/dist/render/sky.js +197 -35
  145. package/dist/render/sky.js.map +1 -1
  146. package/dist/render/weightedblended_oit.js +4 -3
  147. package/dist/render/weightedblended_oit.js.map +1 -1
  148. package/dist/scene/basesprite.js +2 -0
  149. package/dist/scene/basesprite.js.map +1 -1
  150. package/dist/scene/batchgroup.js +10 -0
  151. package/dist/scene/batchgroup.js.map +1 -1
  152. package/dist/scene/environment.js +61 -2
  153. package/dist/scene/environment.js.map +1 -1
  154. package/dist/scene/light.js +178 -10
  155. package/dist/scene/light.js.map +1 -1
  156. package/dist/scene/meshdrawable.js +89 -2
  157. package/dist/scene/meshdrawable.js.map +1 -1
  158. package/dist/scene/msdftext.js +2 -0
  159. package/dist/scene/msdftext.js.map +1 -1
  160. package/dist/scene/msdftextsprite.js +2 -0
  161. package/dist/scene/msdftextsprite.js.map +1 -1
  162. package/dist/scene/particlesys.js +2 -0
  163. package/dist/scene/particlesys.js.map +1 -1
  164. package/dist/scene/scene.js +34 -1
  165. package/dist/scene/scene.js.map +1 -1
  166. package/dist/scene/terrain-cm/grass.js +2 -1
  167. package/dist/scene/terrain-cm/grass.js.map +1 -1
  168. package/dist/scene/terrain-cm/terrain-cm.js +2 -0
  169. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  170. package/dist/scene/water.js +2 -0
  171. package/dist/scene/water.js.map +1 -1
  172. package/dist/shaders/atmosphere.js +5 -4
  173. package/dist/shaders/atmosphere.js.map +1 -1
  174. package/dist/shaders/fog.js +10 -3
  175. package/dist/shaders/fog.js.map +1 -1
  176. package/dist/shaders/shadow.js +96 -29
  177. package/dist/shaders/shadow.js.map +1 -1
  178. package/dist/shaders/ssgi.js +63 -0
  179. package/dist/shaders/ssgi.js.map +1 -0
  180. package/dist/shaders/ssgi_reprojection.js +118 -0
  181. package/dist/shaders/ssgi_reprojection.js.map +1 -0
  182. package/dist/shaders/ssr.js +322 -154
  183. package/dist/shaders/ssr.js.map +1 -1
  184. package/dist/shadow/esm.js +13 -8
  185. package/dist/shadow/esm.js.map +1 -1
  186. package/dist/shadow/pcf_opt.js +15 -12
  187. package/dist/shadow/pcf_opt.js.map +1 -1
  188. package/dist/shadow/pcf_pd.js +15 -12
  189. package/dist/shadow/pcf_pd.js.map +1 -1
  190. package/dist/shadow/pcss.js +17 -11
  191. package/dist/shadow/pcss.js.map +1 -1
  192. package/dist/shadow/shadow_impl.js +20 -2
  193. package/dist/shadow/shadow_impl.js.map +1 -1
  194. package/dist/shadow/shadowmapper.js +10 -3
  195. package/dist/shadow/shadowmapper.js.map +1 -1
  196. package/dist/shadow/ssm.js +16 -13
  197. package/dist/shadow/ssm.js.map +1 -1
  198. package/dist/shadow/vsm.js +10 -8
  199. package/dist/shadow/vsm.js.map +1 -1
  200. package/dist/utility/physical.js +74 -0
  201. package/dist/utility/physical.js.map +1 -0
  202. package/dist/utility/rendermipmap.js +2 -2
  203. package/dist/utility/rendermipmap.js.map +1 -1
  204. package/dist/utility/serialization/manager.js +3 -1
  205. package/dist/utility/serialization/manager.js.map +1 -1
  206. package/dist/utility/serialization/scene/animation.js +3 -1
  207. package/dist/utility/serialization/scene/animation.js.map +1 -1
  208. package/dist/utility/serialization/scene/batch.js +3 -1
  209. package/dist/utility/serialization/scene/batch.js.map +1 -1
  210. package/dist/utility/serialization/scene/camera.js +598 -2
  211. package/dist/utility/serialization/scene/camera.js.map +1 -1
  212. package/dist/utility/serialization/scene/light.js +180 -9
  213. package/dist/utility/serialization/scene/light.js.map +1 -1
  214. package/dist/utility/serialization/scene/material.js +43 -3
  215. package/dist/utility/serialization/scene/material.js.map +1 -1
  216. package/dist/utility/serialization/scene/mesh.js +3 -1
  217. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  218. package/dist/utility/serialization/scene/node.js +3 -1
  219. package/dist/utility/serialization/scene/node.js.map +1 -1
  220. package/dist/utility/serialization/scene/particle.js +3 -1
  221. package/dist/utility/serialization/scene/particle.js.map +1 -1
  222. package/dist/utility/serialization/scene/primitive.js +2 -0
  223. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  224. package/dist/utility/serialization/scene/scene.js +70 -0
  225. package/dist/utility/serialization/scene/scene.js.map +1 -1
  226. package/dist/utility/serialization/scene/sprite.js +3 -1
  227. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  228. package/dist/utility/serialization/scene/terrain.js +3 -1
  229. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  230. package/dist/utility/serialization/scene/text.js +3 -0
  231. package/dist/utility/serialization/scene/text.js.map +1 -1
  232. package/dist/utility/serialization/scene/water.js +3 -1
  233. package/dist/utility/serialization/scene/water.js.map +1 -1
  234. package/dist/values.js +2 -3
  235. package/dist/values.js.map +1 -1
  236. package/package.json +3 -3
  237. package/dist/animation/joint_dynamics/convex_collider.js +0 -320
  238. package/dist/animation/joint_dynamics/convex_collider.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { Vector4 } from '@zephyr3d/base';
1
+ import { REVERSE_Z, 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';
@@ -11,13 +11,18 @@ 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
15
  import { PostEffectLayer, AbstractPostEffect } from '../../posteffect/posteffect.js';
15
16
  import { RenderGraph } from './rendergraph.js';
16
17
  import { RenderGraphExecutor } from './executor.js';
18
+ import { RGTextureAffinityCache } from './texture_affinity_cache.js';
17
19
  import { DevicePoolAllocator } from './device_pool_allocator.js';
18
20
  import { HistoryResourceManager } from './history_resource_manager.js';
19
21
  import { RGHistoryResources } from './history_resources.js';
20
22
  import { FrameResources, RGBlackboard } from './blackboard.js';
23
+ import { OrderingScope } from './render_context.js';
24
+ import { RenderPipeline } from './render_pipeline.js';
25
+ import { mergeFrameResourceRequirements } from './frame_resource_requirements.js';
21
26
  import { renderObjectColors } from '../gpu_picking.js';
22
27
  import { BoxShape } from '../../shapes/box.js';
23
28
  import '../../shapes/capsule.js';
@@ -27,16 +32,28 @@ import '../../shapes/plane.js';
27
32
  import '../../shapes/sphere.js';
28
33
  import '../../shapes/tetrahedron.js';
29
34
 
30
- // ─── Shared Pass Instances ──────────────────────────────────────────
31
35
  const _scenePass = new LightPass();
32
36
  const _depthPass = new DepthPass();
33
37
  const _shadowMapPass = new ShadowMapPass();
34
38
  const _clusters = [];
35
39
  const _shadowMaskRenderer = new ShadowMaskRenderer();
36
40
  const _devicePoolAllocator = new DevicePoolAllocator();
41
+ const _textureAffinityCaches = new WeakMap();
42
+ function getTextureAffinityCache(camera, device) {
43
+ const current = _textureAffinityCaches.get(camera);
44
+ if (current?.device === device) {
45
+ return current.cache;
46
+ }
47
+ const cache = new RGTextureAffinityCache();
48
+ _textureAffinityCaches.set(camera, {
49
+ device,
50
+ cache
51
+ });
52
+ return cache;
53
+ }
37
54
  let _backDepthColorState = null;
38
55
  let _frontDepthColorState = null;
39
- const SURFACE_MRT_FLAGS = MaterialVaryingFlags.SSR_STORE_ROUGHNESS | MaterialVaryingFlags.SSS_STORE_PROFILE | MaterialVaryingFlags.SSS_STORE_DIFFUSE | MaterialVaryingFlags.SSS_STORE_NORMAL | MaterialVaryingFlags.SSS_STORE_TRANSMISSION | MaterialVaryingFlags.SKIN_SSS_STORE;
56
+ const SURFACE_MRT_FLAGS = MaterialVaryingFlags.SCENE_STORE_ROUGHNESS | MaterialVaryingFlags.SSS_STORE_PROFILE | MaterialVaryingFlags.SSS_STORE_DIFFUSE | MaterialVaryingFlags.SCENE_STORE_NORMAL | MaterialVaryingFlags.SSS_STORE_TRANSMISSION | MaterialVaryingFlags.SKIN_SSS_STORE;
40
57
  function getClusteredLight() {
41
58
  return _clusters.length > 0 ? _clusters.pop() : new ClusteredLight();
42
59
  }
@@ -164,23 +181,26 @@ function getSurfaceTextureFormat(ctx) {
164
181
  function getTextureFormatBytes(ctx, format) {
165
182
  return ctx.device.getDeviceCaps().textureCaps.getTextureFormatInfo(format).size;
166
183
  }
184
+ function supportsSSGIRenderTargets(ctx) {
185
+ const caps = ctx.device.getDeviceCaps();
186
+ const rgba16fBytes = caps.textureCaps.getTextureFormatInfo('rgba16f').size;
187
+ return !!(caps.textureCaps.supportHalfFloatColorBuffer && caps.framebufferCaps.maxDrawBuffers >= 2 && caps.framebufferCaps.maxColorAttachmentBytesPerSample >= rgba16fBytes * 2);
188
+ }
167
189
  function shouldStoreSSSDiffuse(ctx) {
168
190
  return ctx.camera.sssStrength > 0 && ctx.camera.sssBlurScale > 0;
169
191
  }
170
192
  function shouldStoreSSSTransmission(ctx) {
171
193
  return ctx.camera.sssStrength > 0 && ctx.camera.sssTransmissionStrength > 0;
172
194
  }
173
- function getSSSLightingTextureFormat(ctx, attachmentCount, includeSSRSurfaceMRT) {
195
+ function getSSSLightingTextureFormat(ctx, attachmentCount, surfaceAttachmentCount) {
174
196
  const colorFormat = ctx.colorFormat;
175
- if (!includeSSRSurfaceMRT || attachmentCount === 0) {
197
+ if (surfaceAttachmentCount === 0 || attachmentCount === 0) {
176
198
  return colorFormat;
177
199
  }
178
200
  const caps = ctx.device.getDeviceCaps();
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.
201
+ // MRT graph textures are not resolved on DrawContext yet.
182
202
  const surfaceFormat = getSurfaceTextureFormat(ctx);
183
- const colorBytes = getTextureFormatBytes(ctx, colorFormat) + getTextureFormatBytes(ctx, surfaceFormat) + getTextureFormatBytes(ctx, surfaceFormat);
203
+ const colorBytes = getTextureFormatBytes(ctx, colorFormat) + getTextureFormatBytes(ctx, surfaceFormat) * surfaceAttachmentCount;
184
204
  const fullPrecisionBytes = colorBytes + getTextureFormatBytes(ctx, colorFormat) * attachmentCount;
185
205
  if (fullPrecisionBytes <= caps.framebufferCaps.maxColorAttachmentBytesPerSample) {
186
206
  return colorFormat;
@@ -197,10 +217,11 @@ function getLightPassColorAttachments(ctx, colorAttachment) {
197
217
  const attachments = [
198
218
  colorAttachment
199
219
  ];
200
- if (ctx.materialFlags & MaterialVaryingFlags.SSR_STORE_ROUGHNESS) {
201
- attachments.push(ctx.SSRRoughnessTexture, ctx.SSRNormalTexture);
202
- } else if (ctx.materialFlags & MaterialVaryingFlags.SSS_STORE_NORMAL) {
203
- attachments.push(ctx.SSRNormalTexture);
220
+ if (ctx.materialFlags & MaterialVaryingFlags.SCENE_STORE_ROUGHNESS) {
221
+ attachments.push(ctx.SceneRoughnessTexture);
222
+ }
223
+ if (ctx.materialFlags & MaterialVaryingFlags.SCENE_STORE_NORMAL) {
224
+ attachments.push(ctx.SceneNormalTexture);
204
225
  }
205
226
  if (ctx.materialFlags & MaterialVaryingFlags.SSS_STORE_PROFILE) {
206
227
  attachments.push(ctx.SSSProfileTexture, ctx.SSSParamTexture);
@@ -216,89 +237,143 @@ function getLightPassColorAttachments(ctx, colorAttachment) {
216
237
  }
217
238
  return attachments.length === 1 ? attachments[0] : attachments;
218
239
  }
219
- /**
220
- * Derive pipeline options from the current scene/camera state.
221
- * @internal
222
- */ function deriveForwardPlusOptions(scene, camera, deviceType, renderQueue) {
240
+ /** Derive Forward+ options from scene and camera state. @internal */ function deriveForwardPlusOptions(scene, camera, deviceType, renderQueue) {
223
241
  const ssr = camera.SSR && scene.env.light.envLight && scene.env.light.envLight.hasRadiance();
242
+ const ssgi = camera.SSGI && camera.HDR && camera.ssgiIntensity > 0 && scene.env.light.type === 'ibl' && !!scene.env.light.envLight?.hasRadiance() && !!scene.env.light.envLight?.hasIrradiance();
224
243
  const sss = camera.SSS && renderQueueHasActiveSSS(renderQueue);
225
244
  const skinSSS = camera.skinSSS && renderQueueHasActiveSkinSSS(renderQueue);
226
245
  const needSceneColor = renderQueue.needSceneColor();
227
246
  const needSceneColorWithDepth = renderQueue.needSceneColorWithDepth();
228
247
  return {
229
248
  depthPrepass: true,
230
- motionVectors: deviceType !== 'webgl' && (camera.TAA || camera.motionBlur || !!ssr && camera.ssrTemporal),
231
- hiZ: camera.HiZ && deviceType !== 'webgl',
249
+ motionVectors: false,
250
+ hiZ: false,
251
+ sceneNormal: false,
252
+ sceneRoughness: false,
253
+ shadowMask: false,
232
254
  ssr: !!ssr,
255
+ ssgi: !!ssgi,
233
256
  ssrCalcThickness: !!ssr && camera.ssrCalcThickness,
234
257
  gpuPicking: !!camera.getPickResultResolveFunc(),
235
258
  needSceneColor,
236
259
  needSceneColorWithDepth,
237
260
  needsTransmissionDepthForSSR: !!ssr && needSceneColor && !needSceneColorWithDepth,
238
261
  sss: !!sss,
239
- skinSSS: !!skinSSS
262
+ skinSSS: !!skinSSS,
263
+ fogPresents: !!scene.env.sky?.fogPresents
240
264
  };
241
265
  }
242
- // ─── Forward+ Graph Builder ─────────────────────────────────────────
243
- /**
244
- * Constructs a render graph representing the forward+ pipeline.
245
- *
246
- * Each step in the existing `SceneRenderer._renderScene` becomes a graph pass.
247
- * Execute callbacks delegate to the existing rendering code, sharing a mutable
248
- * `FrameState`.
249
- *
250
- * @param graph - The render graph to populate.
251
- * @param ctx - The draw context for this frame.
252
- * @param renderQueue - The culled render queue.
253
- * @param options - Pipeline feature toggles.
254
- * @returns The backbuffer handle (graph output).
255
- *
256
- * @public
257
- */ function buildForwardPlusGraph(graph, ctx, renderQueue, options) {
258
- return buildForwardPlusGraphInternal(graph, ctx, renderQueue, options).backbuffer;
266
+ function resolveFrameResourceRequirements(ctx, options, requirements) {
267
+ const deviceType = ctx.device.type;
268
+ options.motionVectors ||= !!requirements.motionVector;
269
+ options.hiZ ||= !!requirements.hiZ;
270
+ options.sceneNormal ||= !!requirements.sceneNormal;
271
+ options.sceneRoughness ||= !!requirements.sceneRoughness;
272
+ options.shadowMask ||= !!requirements.shadowMask;
273
+ if (options.motionVectors && deviceType === 'webgl') {
274
+ throw new Error('Forward+: MotionVector was requested but is not supported by the WebGL backend.');
275
+ }
276
+ if (options.hiZ && deviceType === 'webgl') {
277
+ throw new Error('Forward+: HiZ was requested but is not supported by the WebGL backend.');
278
+ }
279
+ if (options.shadowMask && deviceType === 'webgl') {
280
+ throw new Error('Forward+: ShadowMask was requested but is not supported by the WebGL backend.');
281
+ }
282
+ const surfaceAttachmentCount = Number(options.sceneNormal) + Number(options.sceneRoughness);
283
+ if (surfaceAttachmentCount > 0) {
284
+ const caps = ctx.device.getDeviceCaps();
285
+ const maxDrawBuffers = caps.framebufferCaps.maxDrawBuffers ?? Number.MAX_SAFE_INTEGER;
286
+ if (1 + surfaceAttachmentCount > maxDrawBuffers) {
287
+ throw new Error(`Forward+: ${surfaceAttachmentCount} scene surface attachment(s) were requested, ` + `but the device supports only ${maxDrawBuffers} color attachments.`);
288
+ }
289
+ const bytesPerSample = getTextureFormatBytes(ctx, ctx.colorFormat) + getTextureFormatBytes(ctx, getSurfaceTextureFormat(ctx)) * surfaceAttachmentCount;
290
+ if (bytesPerSample > caps.framebufferCaps.maxColorAttachmentBytesPerSample) {
291
+ throw new Error(`Forward+: requested scene surface attachments require ${bytesPerSample} bytes per sample, ` + `but the device limit is ${caps.framebufferCaps.maxColorAttachmentBytesPerSample}.`);
292
+ }
293
+ }
294
+ ctx.motionVectors = options.motionVectors;
295
+ ctx.HiZ = options.hiZ;
296
+ ctx.SSGI = options.ssgi;
297
+ ctx.screenSpaceShadowMask = options.shadowMask;
298
+ ctx.SSS = options.sss;
259
299
  }
260
- function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
261
- const backbuffer = graph.importTexture('backbuffer');
262
- ctx.SSS = !!options.sss;
263
- ctx.SkinSSSTexture = null;
264
- // Named registry of shared frame resources (consumed by post effect setup)
265
- const blackboard = new RGBlackboard();
266
- // Shared mutable frame state
267
- const frame = {
268
- ctx,
269
- renderQueue,
270
- depthFramebuffer: null,
271
- sunLightColor: null,
272
- options,
273
- renderQueueDisposed: false,
274
- clusteredLightReleased: false,
275
- sunLightRestored: false
276
- };
277
- // ── 1. Sky Update ─────────────────────────────────────────────────
278
- let orderToken = graph.addPass('SkyUpdate', (builder)=>{
279
- const done = builder.createToken('SkyUpdateDone');
280
- builder.sideEffect();
281
- builder.setExecute(()=>{
282
- frame.sunLightColor = ctx.scene.env.sky.update(ctx);
300
+ function validateProducedFrameResources(blackboard, options, renderQueue) {
301
+ const required = [
302
+ [
303
+ options.motionVectors,
304
+ FrameResources.MotionVector
305
+ ],
306
+ [
307
+ options.hiZ,
308
+ FrameResources.HiZ
309
+ ],
310
+ [
311
+ options.sceneNormal,
312
+ FrameResources.SceneNormal
313
+ ],
314
+ [
315
+ options.sceneRoughness,
316
+ FrameResources.SceneRoughness
317
+ ],
318
+ [
319
+ options.shadowMask && renderQueue.shadowedLights.length > 0,
320
+ FrameResources.ShadowMask
321
+ ]
322
+ ];
323
+ for (const [needed, resource] of required){
324
+ if (needed && !blackboard.has(resource)) {
325
+ throw new Error(`Forward+: frame resource "${resource}" was requested but no enabled RenderModule produced it.`);
326
+ }
327
+ }
328
+ }
329
+ /** Require state produced by an earlier module. @internal */ function requireBuildState(fg, key, producerType, consumerType) {
330
+ const value = fg.state[key];
331
+ if (!value) {
332
+ throw new Error(`Forward+ module "${consumerType}" requires module "${producerType}" ` + `(build state "${String(key)}" was not produced). Did you remove it from the pipeline?`);
333
+ }
334
+ return value;
335
+ }
336
+ /** @internal */ const SkyUpdateModule = {
337
+ type: 'SkyUpdate',
338
+ prepare: ()=>({
339
+ enabled: true
340
+ }),
341
+ setup ({ graph, ctx, frame, ordering }) {
342
+ graph.addPass('SkyUpdate', (builder)=>{
343
+ ordering.emit(builder, 'SkyUpdateDone');
344
+ builder.sideEffect();
345
+ builder.setExecute(()=>{
346
+ frame.sunLightColor = ctx.scene.env.sky.update(ctx);
347
+ });
283
348
  });
284
- return done;
285
- });
286
- // ── 2. Clustered Light Setup ──────────────────────────────────────
287
- orderToken = graph.addPass('ClusterLights', (builder)=>{
288
- builder.read(orderToken);
289
- const done = builder.createToken('ClusterLightsDone');
290
- builder.sideEffect();
291
- builder.setExecute(()=>{
292
- ctx.clusteredLight = getClusteredLight();
293
- ctx.clusteredLight.calculateLightIndex(ctx.camera, renderQueue);
349
+ }
350
+ };
351
+ /** @internal */ const ClusterLightsModule = {
352
+ type: 'ClusterLights',
353
+ prepare: ()=>({
354
+ enabled: true
355
+ }),
356
+ setup ({ graph, ctx, renderQueue, ordering }) {
357
+ graph.addPass('ClusterLights', (builder)=>{
358
+ ordering.chainInto(builder);
359
+ ordering.emit(builder, 'ClusterLightsDone');
360
+ builder.sideEffect();
361
+ builder.setExecute(()=>{
362
+ ctx.clusteredLight = getClusteredLight();
363
+ ctx.clusteredLight.calculateLightIndex(ctx.camera, renderQueue, ctx.screenSpaceShadowMask, ShaderHelper.getPreExposure(ctx));
364
+ });
294
365
  });
295
- return done;
296
- });
297
- // ── 3. GPU Picking (optional, sideEffect) ─────────────────────────
298
- if (options.gpuPicking) {
299
- orderToken = graph.addPass('GPUPicking', (builder)=>{
300
- builder.read(orderToken);
301
- const done = builder.createToken('GPUPickingDone');
366
+ }
367
+ };
368
+ /** @internal */ const GPUPickingModule = {
369
+ type: 'GPUPicking',
370
+ prepare: ({ options })=>({
371
+ enabled: options.gpuPicking
372
+ }),
373
+ setup ({ graph, ctx, renderQueue, ordering }) {
374
+ graph.addPass('GPUPicking', (builder)=>{
375
+ ordering.chainInto(builder);
376
+ ordering.emit(builder, 'GPUPickingDone');
302
377
  builder.sideEffect();
303
378
  builder.setExecute(()=>{
304
379
  const pickResolveFunc = ctx.camera.getPickResultResolveFunc();
@@ -306,125 +381,140 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
306
381
  renderObjectColors(ctx, pickResolveFunc, renderQueue);
307
382
  }
308
383
  });
309
- return done;
310
384
  });
311
385
  }
312
- // ── 4. Shadow Maps ────────────────────────────────────────────────
313
- // Shadow maps are managed internally by lights, mark as side effect
314
- if (renderQueue.shadowedLights.length > 0) {
315
- orderToken = graph.addPass('ShadowMaps', (builder)=>{
316
- builder.read(orderToken);
317
- const done = builder.createToken('ShadowMapsDone');
386
+ };
387
+ /** @internal */ const ShadowMapsModule = {
388
+ type: 'ShadowMaps',
389
+ // Light-owned shadow maps are observable side effects.
390
+ prepare: ({ renderQueue })=>({
391
+ enabled: renderQueue.shadowedLights.length > 0
392
+ }),
393
+ setup ({ graph, ctx, renderQueue, ordering }) {
394
+ graph.addPass('ShadowMaps', (builder)=>{
395
+ ordering.chainInto(builder);
396
+ ordering.emit(builder, 'ShadowMapsDone');
318
397
  builder.sideEffect();
319
398
  builder.setExecute(()=>{
320
399
  renderShadowMaps(ctx, renderQueue.shadowedLights);
321
400
  });
322
- return done;
323
401
  });
324
402
  }
325
- // ── 5. Depth Prepass ──────────────────────────────────────────────
326
- // Declare transient depth and motion vector textures
327
- const depthPassResult = graph.addPass('DepthPrepass', (builder)=>{
328
- builder.read(orderToken);
329
- const format = ctx.device.type === 'webgl' ? ctx.SSRCalcThickness ? 'rgba16f' : 'rgba8unorm' : ctx.SSRCalcThickness ? 'rg32f' : 'r32f';
330
- const mvFormat = 'rgba16f';
331
- const depthHandle = builder.createTexture({
332
- format,
333
- label: 'linearDepth'
334
- });
335
- const motionVectorHandle = options.motionVectors ? builder.createTexture({
336
- format: mvFormat,
337
- label: 'motionVector'
338
- }) : undefined;
339
- const finalDepthAttachment = ctx.finalFramebuffer?.getDepthAttachment();
340
- const externalDepthAttachment = finalDepthAttachment?.isTexture2D() ? finalDepthAttachment : null;
341
- const graphDepthAttachmentHandle = externalDepthAttachment ? undefined : builder.createTexture({
342
- format: ctx.depthFormat,
343
- label: 'sceneDepth'
344
- });
345
- const depthAttachmentOrFormat = externalDepthAttachment ?? graphDepthAttachmentHandle ?? ctx.depthFormat;
346
- const depthFramebufferHandle = builder.createFramebuffer({
347
- label: 'DepthPrepassFramebuffer',
348
- width: ctx.renderWidth,
349
- height: ctx.renderHeight,
350
- colorAttachments: motionVectorHandle ? [
403
+ };
404
+ /** @internal */ const DepthPrepassModule = {
405
+ type: 'DepthPrepass',
406
+ writes: [
407
+ FrameResources.LinearDepth,
408
+ FrameResources.MotionVector,
409
+ FrameResources.SceneDepthAttachment
410
+ ],
411
+ prepare: ()=>({
412
+ enabled: true
413
+ }),
414
+ setup (fg) {
415
+ const { graph, ctx, frame, ordering, blackboard, options } = fg;
416
+ const result = graph.addPass('DepthPrepass', (builder)=>{
417
+ ordering.chainInto(builder);
418
+ const format = ctx.device.type === 'webgl' ? ctx.SSRCalcThickness ? 'rgba16f' : 'rgba8unorm' : ctx.SSRCalcThickness ? 'rg32f' : 'r32f';
419
+ const mvFormat = 'rgba16f';
420
+ const depthHandle = builder.createTexture({
421
+ format,
422
+ label: 'linearDepth',
423
+ allocationKey: 'ForwardPlus.LinearDepth'
424
+ });
425
+ const motionVectorHandle = options.motionVectors ? builder.createTexture({
426
+ format: mvFormat,
427
+ label: 'motionVector',
428
+ allocationKey: 'ForwardPlus.MotionVector'
429
+ }) : undefined;
430
+ const finalDepthAttachment = ctx.finalFramebuffer?.getDepthAttachment();
431
+ const externalDepthAttachment = finalDepthAttachment?.isTexture2D() ? finalDepthAttachment : null;
432
+ const externalDepthAttachmentHandle = externalDepthAttachment ? graph.importTexture('externalSceneDepth') : undefined;
433
+ const graphDepthAttachmentHandle = externalDepthAttachment ? undefined : builder.createTexture({
434
+ format: ctx.depthFormat,
435
+ label: 'sceneDepth',
436
+ allocationKey: 'ForwardPlus.SceneDepth'
437
+ });
438
+ const depthAttachmentOrFormat = externalDepthAttachmentHandle ?? graphDepthAttachmentHandle ?? ctx.depthFormat;
439
+ const depthFramebufferHandle = builder.createFramebuffer({
440
+ label: 'DepthPrepassFramebuffer',
441
+ width: ctx.renderWidth,
442
+ height: ctx.renderHeight,
443
+ colorAttachments: motionVectorHandle ? [
444
+ depthHandle,
445
+ motionVectorHandle
446
+ ] : depthHandle,
447
+ depthAttachment: depthAttachmentOrFormat,
448
+ ignoreDepthStencil: false
449
+ });
450
+ const skyMotionVectorFramebufferHandle = motionVectorHandle ? builder.createFramebuffer({
451
+ label: 'SkyMotionVectorFramebuffer',
452
+ width: ctx.renderWidth,
453
+ height: ctx.renderHeight,
454
+ colorAttachments: motionVectorHandle,
455
+ depthAttachment: depthAttachmentOrFormat
456
+ }) : undefined;
457
+ builder.addSubpass('SceneDepth', (rgCtx)=>{
458
+ const depthFramebuffer = rgCtx.getFramebuffer(depthFramebufferHandle);
459
+ frame.depthFramebuffer = renderSceneDepth(frame, depthFramebuffer, rgCtx, undefined, undefined, false);
460
+ });
461
+ if (skyMotionVectorFramebufferHandle) {
462
+ builder.addSubpass('SkyMotionVectors', (rgCtx)=>{
463
+ renderSkyMotionVectors(ctx, rgCtx, skyMotionVectorFramebufferHandle);
464
+ });
465
+ }
466
+ return {
351
467
  depthHandle,
352
- motionVectorHandle
353
- ] : depthHandle,
354
- depthAttachment: depthAttachmentOrFormat,
355
- ignoreDepthStencil: false
356
- });
357
- const skyMotionVectorFramebufferHandle = motionVectorHandle ? builder.createFramebuffer({
358
- label: 'SkyMotionVectorFramebuffer',
359
- width: ctx.renderWidth,
360
- height: ctx.renderHeight,
361
- colorAttachments: motionVectorHandle,
362
- depthAttachment: depthAttachmentOrFormat
363
- }) : undefined;
364
- builder.addSubpass('SceneDepth', (rgCtx)=>{
365
- const depthFramebuffer = rgCtx.getFramebuffer(depthFramebufferHandle);
366
- frame.depthFramebuffer = renderSceneDepth(frame, depthFramebuffer, rgCtx, undefined, undefined, false);
468
+ motionVectorHandle,
469
+ graphDepthAttachmentHandle,
470
+ externalDepthAttachmentHandle,
471
+ externalDepthAttachment,
472
+ depthFramebufferHandle
473
+ };
367
474
  });
368
- if (skyMotionVectorFramebufferHandle) {
369
- builder.addSubpass('SkyMotionVectors', (rgCtx)=>{
370
- renderSkyMotionVectors(ctx, rgCtx, skyMotionVectorFramebufferHandle);
371
- });
475
+ fg.state.depth = result;
476
+ // Depth mutations re-register their versions through the blackboard.
477
+ blackboard.set(FrameResources.LinearDepth, result.depthHandle);
478
+ if (result.motionVectorHandle) {
479
+ blackboard.set(FrameResources.MotionVector, result.motionVectorHandle);
372
480
  }
373
- return {
374
- depthHandle,
375
- motionVectorHandle,
376
- graphDepthAttachmentHandle,
377
- externalDepthAttachment,
378
- depthFramebufferHandle
379
- };
380
- });
381
- let depthHandle = depthPassResult.depthHandle;
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);
481
+ blackboard.set(FrameResources.SceneDepthAttachment, result.graphDepthAttachmentHandle ?? result.externalDepthAttachmentHandle);
482
+ // Later modules share this attachment without owning it.
483
+ fg.state.renderDepthAttachment = result.graphDepthAttachmentHandle ?? result.externalDepthAttachmentHandle ?? null;
484
+ // Opaque effects require a texture and may require surface MRT attachments.
485
+ const opaqueLayerHasEffects = !!ctx.compositor?.layerHasEnabledEffect(PostEffectLayer.opaque);
486
+ fg.state.useFinalFramebufferAsIntermediate = !!result.externalDepthAttachment && result.externalDepthAttachment === ctx.finalFramebuffer?.getDepthAttachment() && !opaqueLayerHasEffects;
389
487
  }
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) {
488
+ };
489
+ /** @internal */ const ShadowMaskModule = {
490
+ type: 'ShadowMaskPass',
491
+ writes: [
492
+ FrameResources.ShadowMask
493
+ ],
494
+ // shadowMapInfo is populated at execute time and cannot gate graph building.
495
+ prepare: ({ ctx, options, renderQueue })=>({
496
+ enabled: options.shadowMask && renderQueue.shadowedLights.length > 0,
497
+ requirements: {
498
+ shadowMask: ctx.device.type !== 'webgl' && ctx.camera.screenSpaceShadowMask && renderQueue.shadowedLights.length > 0
499
+ }
500
+ }),
501
+ setup (fg) {
502
+ // Four shadow lights share each RGBA8 layer in clustered-light order.
503
+ const { graph, ctx, renderQueue, blackboard } = fg;
504
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'ShadowMaskPass');
407
505
  const numShadowLights = renderQueue.shadowedLights.length;
408
506
  const numLayers = ShadowMaskRenderer.getLayerCount(numShadowLights);
409
507
  const maskPassResult = graph.addPass('ShadowMaskPass', (builder)=>{
410
- // Freeze the current linear-depth handle at build time. `depthHandle` is a
411
- // mutable `let` that a later TransmissionDepth pass reassigns to a new
412
- // version ('linearDepth@TransmissionDepth'); if the execute closure below
413
- // captured it by reference it would read a handle this pass never declared,
414
- // tripping RenderGraphExecutor's access assertion. The shadow mask is built
415
- // from the opaque prepass depth, so the pre-transmission version is correct.
416
- const maskDepthHandle = depthHandle;
508
+ // Capture the depth version declared at this pipeline position.
509
+ const maskDepthHandle = blackboard.expect(FrameResources.LinearDepth);
417
510
  builder.read(maskDepthHandle);
418
511
  builder.read(depthPassResult.depthFramebufferHandle);
419
- // createTexture already registers this pass as the resource producer, so
420
- // downstream passes read this handle directly (same pattern as HiZ). The
421
- // per-layer framebuffers are created inside execute (rgCtx.createFramebuffer),
422
- // which the graph cannot see; the createTexture producer edge is what keeps
423
- // the resource alive and ordered before the LightPass reader.
512
+ // Per-layer framebuffers are temporary views of this graph texture.
424
513
  const maskHandle = builder.createTexture({
425
514
  format: 'rgba8unorm',
426
515
  label: 'shadowMask',
427
- arrayLayers: numLayers
516
+ arrayLayers: numLayers,
517
+ allocationKey: 'ForwardPlus.ShadowMask'
428
518
  });
429
519
  builder.setExecute((rgCtx)=>{
430
520
  const depthTex = rgCtx.getTexture(maskDepthHandle);
@@ -436,34 +526,31 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
436
526
  depthAttachment: null,
437
527
  attachmentLayer: layer
438
528
  }));
439
- ctx.shadowMaskTexture = maskTex;
440
529
  });
441
530
  return {
442
531
  maskHandle
443
532
  };
444
533
  });
445
- shadowMaskHandle = maskPassResult.maskHandle;
446
- blackboard.set(FrameResources.ShadowMask, shadowMaskHandle);
534
+ blackboard.set(FrameResources.ShadowMask, maskPassResult.maskHandle);
447
535
  }
448
- const renderDepthAttachment = depthPassResult.graphDepthAttachmentHandle ?? depthPassResult.externalDepthAttachment ?? null;
449
- // Rendering the scene directly into the final framebuffer is only possible
450
- // when no opaque-layer effect is enabled: those effects must sample the
451
- // opaque scene color as a texture and may require surface MRT attachments
452
- // (SSR roughness / SSS / SkinSSS), which the single-color final framebuffer
453
- // cannot carry. The legacy compositor.begin() used to redirect rendering to
454
- // a temporal MRT framebuffer in that case; the graph path uses the regular
455
- // scene color texture pipeline instead.
456
- const opaqueLayerHasEffects = !!ctx.compositor?.layerHasEnabledEffect(PostEffectLayer.opaque);
457
- const useFinalFramebufferAsIntermediate = !!depthPassResult.externalDepthAttachment && depthPassResult.externalDepthAttachment === ctx.finalFramebuffer?.getDepthAttachment() && !opaqueLayerHasEffects;
458
- let preLightTransmissionDepthToken;
459
- if (options.needsTransmissionDepthForSSR) {
536
+ };
537
+ /** @internal */ const TransmissionDepthForSSRModule = {
538
+ type: 'TransmissionDepthForSSR',
539
+ writes: [
540
+ FrameResources.LinearDepth
541
+ ],
542
+ prepare: ({ options })=>({
543
+ enabled: options.needsTransmissionDepthForSSR
544
+ }),
545
+ setup (fg) {
546
+ const { graph, frame, blackboard } = fg;
547
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'TransmissionDepthForSSR');
460
548
  const transmissionDepthResult = graph.addPass('TransmissionDepthForSSR', (builder)=>{
461
- builder.read(depthHandle);
549
+ const currentDepth = blackboard.expect(FrameResources.LinearDepth);
550
+ builder.read(currentDepth);
462
551
  builder.read(depthPassResult.depthFramebufferHandle);
463
- // This pass renders transmission geometry into the prepass linear-depth
464
- // texture: model the mutation as a write so later readers order against
465
- // it through data flow instead of relying on the token alone.
466
- const depthOut = builder.write(depthHandle);
552
+ // Version the in-place depth mutation for downstream dependencies.
553
+ const depthOut = builder.write(currentDepth);
467
554
  const done = builder.createToken('TransmissionDepthForSSRDone');
468
555
  builder.sideEffect();
469
556
  builder.setExecute((rgCtx)=>{
@@ -474,24 +561,38 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
474
561
  depthOut
475
562
  };
476
563
  });
477
- preLightTransmissionDepthToken = transmissionDepthResult.done;
478
- depthHandle = transmissionDepthResult.depthOut;
479
- // Re-register so blackboard consumers read the post-transmission version.
480
- blackboard.set(FrameResources.LinearDepth, depthHandle);
564
+ fg.state.preLightTransmissionDepthToken = transmissionDepthResult.done;
565
+ // Publish the post-transmission depth version.
566
+ blackboard.set(FrameResources.LinearDepth, transmissionDepthResult.depthOut);
481
567
  }
482
- // ── 6. Hi-Z (optional) ───────────────────────────────────────────
483
- let hiZHandle;
484
- if (options.hiZ) {
568
+ };
569
+ /** @internal */ const HiZModule = {
570
+ type: 'HiZ',
571
+ writes: [
572
+ FrameResources.HiZ
573
+ ],
574
+ prepare: ({ options })=>({
575
+ enabled: options.hiZ
576
+ }),
577
+ setup (fg) {
578
+ const { graph, ctx, frame, blackboard } = fg;
579
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'HiZ');
580
+ const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
581
+ //const hiZWidth = nextPowerOf2(ctx.renderWidth);
582
+ //const hiZHeight = nextPowerOf2(ctx.renderHeight);
583
+ let hiZHandle;
485
584
  graph.addPass('HiZ', (builder)=>{
486
- builder.read(depthHandle);
585
+ builder.read(blackboard.expect(FrameResources.LinearDepth));
487
586
  builder.read(depthPassResult.depthFramebufferHandle);
488
587
  if (preLightTransmissionDepthToken) {
489
588
  builder.read(preLightTransmissionDepthToken);
490
589
  }
491
590
  hiZHandle = builder.createTexture({
492
- format: 'rg32f',
591
+ format: REVERSE_Z ? 'r16f' : 'r32f',
493
592
  label: 'hiZ',
494
- mipLevels: getFullMipLevelCount(ctx.renderWidth, ctx.renderHeight)
593
+ sizeMode: 'backbuffer-relative',
594
+ mipLevels: getFullMipLevelCount(ctx.renderWidth, ctx.renderHeight),
595
+ allocationKey: 'ForwardPlus.HiZ'
495
596
  });
496
597
  const hiZFramebufferHandle = builder.createFramebuffer({
497
598
  label: 'HiZFramebuffer',
@@ -499,79 +600,55 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
499
600
  depthAttachment: null
500
601
  });
501
602
  builder.setExecute((rgCtx)=>{
502
- const ctx = frame.ctx;
503
- // Use the depth texture from the framebuffer (which contains the RenderGraph texture)
504
- const depthTex = frame.depthFramebuffer?.getDepthAttachment();
505
- if (depthTex) {
506
- // Get the HiZ texture allocated by the executor
507
- const hiZTex = rgCtx.getTexture(hiZHandle);
508
- const HiZFrameBuffer = rgCtx.getFramebuffer(hiZFramebufferHandle);
509
- buildHiZ(depthTex, HiZFrameBuffer);
510
- ctx.HiZTexture = hiZTex;
603
+ const passCtx = frame.ctx;
604
+ // Resolve the declared handle, not mutable framebuffer state.
605
+ const depthFb = rgCtx.getFramebuffer(depthPassResult.depthFramebufferHandle);
606
+ const depthTex = depthFb.getDepthAttachment();
607
+ if (!depthTex) {
608
+ throw new Error('HiZ pass: depth prepass framebuffer has no depth attachment.');
511
609
  }
610
+ const hiZTex = rgCtx.getTexture(hiZHandle);
611
+ const HiZFrameBuffer = rgCtx.getFramebuffer(hiZFramebufferHandle);
612
+ buildHiZ(depthTex, HiZFrameBuffer);
613
+ passCtx.HiZTexture = hiZTex;
512
614
  });
513
615
  });
514
616
  if (hiZHandle) {
515
617
  blackboard.set(FrameResources.HiZ, hiZHandle);
516
618
  }
517
619
  }
518
- // ── 7. Main Light Pass ────────────────────────────────────────────
519
- const historyManager = ctx.camera?.getHistoryResourceManager?.() ?? null;
520
- const lightHistoryReadBindings = [];
521
- const historySize = {
522
- width: ctx.renderWidth,
523
- height: ctx.renderHeight
524
- };
525
- if (historyManager && options.ssr && ctx.camera?.ssrTemporal && options.motionVectors) {
526
- const reflectHistoryHandle = historyManager.importPreviousIfCompatible(graph, RGHistoryResources.SSR_REFLECT, {
527
- format: 'rgba16f',
528
- sizeMode: 'absolute',
529
- width: ctx.renderWidth,
530
- height: ctx.renderHeight
531
- }, historySize);
532
- const motionVectorHistoryHandle = historyManager.importPreviousIfCompatible(graph, RGHistoryResources.SSR_MOTION_VECTOR, {
533
- format: 'rgba16f',
534
- sizeMode: 'absolute',
535
- width: ctx.renderWidth,
536
- height: ctx.renderHeight
537
- }, historySize);
538
- if (reflectHistoryHandle && motionVectorHistoryHandle) {
539
- lightHistoryReadBindings.push({
540
- name: RGHistoryResources.SSR_REFLECT,
541
- handle: reflectHistoryHandle
542
- }, {
543
- name: RGHistoryResources.SSR_MOTION_VECTOR,
544
- handle: motionVectorHistoryHandle
545
- });
546
- }
547
- }
548
- // Note: TAA history import/commit is handled by TAA.setup() (self-describing).
549
- let sssProfileResult;
550
- if (options.sss) {
551
- sssProfileResult = graph.addPass('SSSProfile', (builder)=>{
552
- builder.read(depthHandle);
620
+ };
621
+ /** @internal */ const SSSProfileModule = {
622
+ type: 'SSSProfile',
623
+ prepare: ({ options })=>({
624
+ enabled: options.sss
625
+ }),
626
+ setup (fg) {
627
+ const { graph, ctx, frame, blackboard } = fg;
628
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SSSProfile');
629
+ const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
630
+ const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
631
+ const renderDepthAttachment = fg.state.renderDepthAttachment;
632
+ const sssProfileResult = graph.addPass('SSSProfile', (builder)=>{
633
+ builder.read(blackboard.expect(FrameResources.LinearDepth));
553
634
  builder.read(depthPassResult.depthFramebufferHandle);
554
635
  if (preLightTransmissionDepthToken) {
555
636
  builder.read(preLightTransmissionDepthToken);
556
637
  }
638
+ if (shadowMaskHandle) {
639
+ builder.read(shadowMaskHandle);
640
+ }
557
641
  const profileHandle = builder.createTexture({
558
642
  format: 'rgba16f',
559
- label: 'sssProfile'
643
+ label: 'sssProfile',
644
+ allocationKey: 'ForwardPlus.SSSProfile'
560
645
  });
561
646
  const paramHandle = builder.createTexture({
562
647
  format: 'rgba8unorm',
563
- label: 'sssParam'
648
+ label: 'sssParam',
649
+ allocationKey: 'ForwardPlus.SSSParam'
564
650
  });
565
- const normalHandle = options.ssr ? undefined : builder.createTexture({
566
- format: getSurfaceTextureFormat(ctx),
567
- label: 'sssNormal'
568
- });
569
- const colorAttachments = normalHandle ? [
570
- ctx.colorFormat,
571
- normalHandle,
572
- profileHandle,
573
- paramHandle
574
- ] : [
651
+ const colorAttachments = [
575
652
  ctx.colorFormat,
576
653
  profileHandle,
577
654
  paramHandle
@@ -585,34 +662,46 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
585
662
  ignoreDepthStencil: false
586
663
  });
587
664
  builder.setExecute((rgCtx)=>{
588
- renderForwardSSSProfile(frame, rgCtx.getFramebuffer(framebufferHandle), rgCtx.getTexture(profileHandle), rgCtx.getTexture(paramHandle), normalHandle ? rgCtx.getTexture(normalHandle) : null);
665
+ ctx.shadowMaskTexture = shadowMaskHandle ? rgCtx.getTexture(shadowMaskHandle) : null;
666
+ renderForwardSSSProfile(frame, rgCtx.getFramebuffer(framebufferHandle), rgCtx.getTexture(profileHandle), rgCtx.getTexture(paramHandle), null);
589
667
  });
590
668
  return {
591
669
  profileHandle,
592
670
  paramHandle,
593
- normalHandle,
594
671
  framebufferHandle
595
672
  };
596
673
  });
674
+ blackboard.set(FrameResources.SSSProfile, sssProfileResult.profileHandle);
675
+ blackboard.set(FrameResources.SSSParam, sssProfileResult.paramHandle);
597
676
  }
598
- // 7b. Scene color grab: renders the full scene (no transmission) into a copy
599
- // texture that transmission/refraction materials sample as background.
600
- // Extracted from the former monolithic light pass.
601
- let grabResult;
602
- if (options.needSceneColor) {
603
- grabResult = graph.addPass('SceneColorGrab', (builder)=>{
604
- builder.read(depthHandle);
677
+ };
678
+ /** @internal */ const SceneColorGrabModule = {
679
+ type: 'SceneColorGrab',
680
+ prepare: ({ options })=>({
681
+ enabled: options.needSceneColor
682
+ }),
683
+ setup (fg) {
684
+ // Refraction samples this non-transmission scene copy.
685
+ const { graph, ctx, frame, blackboard, options } = fg;
686
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SceneColorGrab');
687
+ const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
688
+ const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
689
+ const renderDepthAttachment = fg.state.renderDepthAttachment;
690
+ const grabResult = graph.addPass('SceneColorGrab', (builder)=>{
691
+ builder.read(blackboard.expect(FrameResources.LinearDepth));
605
692
  builder.read(depthPassResult.depthFramebufferHandle);
606
693
  if (preLightTransmissionDepthToken) {
607
694
  builder.read(preLightTransmissionDepthToken);
608
695
  }
696
+ if (shadowMaskHandle) {
697
+ builder.read(shadowMaskHandle);
698
+ }
609
699
  const copyHandle = builder.createTexture({
610
700
  format: ctx.colorFormat,
611
- label: 'sceneColorCopy'
701
+ label: 'sceneColorCopy',
702
+ allocationKey: 'ForwardPlus.SceneColorCopy'
612
703
  });
613
- // SSR may pre-insert transmission depth before LightPass for Hi-Z. In that
614
- // case the refraction scene-color copy needs an isolated depth buffer so
615
- // transmission surfaces do not occlude the background they sample.
704
+ // Isolate depth when SSR inserts transmission depth before LightPass.
616
705
  const copyFramebufferHandle = !options.needsTransmissionDepthForSSR ? builder.createFramebuffer({
617
706
  label: 'SceneColorCopyFramebuffer',
618
707
  width: ctx.renderWidth,
@@ -622,6 +711,7 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
622
711
  ignoreDepthStencil: false
623
712
  }) : undefined;
624
713
  builder.setExecute((rgCtx)=>{
714
+ ctx.shadowMaskTexture = shadowMaskHandle ? rgCtx.getTexture(shadowMaskHandle) : null;
625
715
  renderSceneColorGrab(frame, rgCtx, copyHandle, copyFramebufferHandle);
626
716
  });
627
717
  return {
@@ -629,366 +719,569 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
629
719
  copyFramebufferHandle
630
720
  };
631
721
  });
722
+ blackboard.set(FrameResources.SceneColorCopy, grabResult.copyHandle);
632
723
  }
633
- const lightPassResult = graph.addPass('LightPass', (builder)=>{
634
- builder.read(depthHandle);
635
- builder.read(depthPassResult.depthFramebufferHandle);
636
- if (shadowMaskHandle) {
637
- builder.read(shadowMaskHandle);
638
- }
639
- if (preLightTransmissionDepthToken) {
640
- builder.read(preLightTransmissionDepthToken);
641
- }
642
- if (hiZHandle) {
643
- builder.read(hiZHandle);
644
- }
645
- for (const binding of lightHistoryReadBindings){
646
- builder.read(binding.handle);
647
- }
648
- // Scene color: in final-framebuffer-as-intermediate mode the scene is
649
- // physically rendered into the final framebuffer, so declare the
650
- // backbuffer write — the graph sees the real data flow and no keep-alive
651
- // reads are needed downstream. Otherwise render into a graph texture.
652
- const sceneColorHandle = useFinalFramebufferAsIntermediate ? builder.write(backbuffer) : builder.createTexture({
653
- format: ctx.colorFormat,
654
- label: 'sceneColor'
655
- });
656
- // Transmission/refraction background produced by the SceneColorGrab pass
657
- const sceneColorCopyHandle = grabResult?.copyHandle;
658
- if (sceneColorCopyHandle) {
659
- builder.read(sceneColorCopyHandle);
660
- }
661
- if (sssProfileResult) {
662
- builder.read(sssProfileResult.profileHandle);
663
- builder.read(sssProfileResult.paramHandle);
664
- if (sssProfileResult.normalHandle) {
665
- builder.read(sssProfileResult.normalHandle);
724
+ };
725
+ /** @internal */ const LightPassModule = {
726
+ type: 'LightPass',
727
+ writes: [
728
+ FrameResources.SceneColor,
729
+ FrameResources.SceneRoughness,
730
+ FrameResources.SceneNormal,
731
+ FrameResources.SSSDiffuse,
732
+ FrameResources.SSSTransmission,
733
+ FrameResources.SkinSSS
734
+ ],
735
+ prepare: ()=>({
736
+ enabled: true
737
+ }),
738
+ setup (fg) {
739
+ const { graph, ctx, frame, blackboard, options, backbuffer } = fg;
740
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'LightPass');
741
+ const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
742
+ const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
743
+ const hiZHandle = blackboard.get(FrameResources.HiZ);
744
+ const sssProfileHandle = blackboard.get(FrameResources.SSSProfile);
745
+ const sssParamHandle = blackboard.get(FrameResources.SSSParam);
746
+ const sceneColorCopyHandle = blackboard.get(FrameResources.SceneColorCopy);
747
+ const useFinalFramebufferAsIntermediate = fg.state.useFinalFramebufferAsIntermediate;
748
+ const renderDepthAttachment = fg.state.renderDepthAttachment;
749
+ const historyManager = fg.history;
750
+ // Keep SSR history scoped while the light pass executes.
751
+ const lightHistoryReadBindings = fg.state.lightHistoryReadBindings;
752
+ const historySize = {
753
+ width: ctx.renderWidth,
754
+ height: ctx.renderHeight
755
+ };
756
+ if (historyManager && options.ssr && ctx.camera?.ssrTemporal && options.motionVectors) {
757
+ const reflectHistoryHandle = historyManager.importPreviousIfCompatible(graph, RGHistoryResources.SSR_REFLECT, {
758
+ format: 'rgba16f',
759
+ sizeMode: 'absolute',
760
+ width: ctx.renderWidth,
761
+ height: ctx.renderHeight
762
+ }, historySize);
763
+ const motionVectorHistoryHandle = historyManager.importPreviousIfCompatible(graph, RGHistoryResources.SSR_MOTION_VECTOR, {
764
+ format: 'rgba16f',
765
+ sizeMode: 'absolute',
766
+ width: ctx.renderWidth,
767
+ height: ctx.renderHeight
768
+ }, historySize);
769
+ if (reflectHistoryHandle && motionVectorHistoryHandle) {
770
+ lightHistoryReadBindings.push({
771
+ name: RGHistoryResources.SSR_REFLECT,
772
+ handle: reflectHistoryHandle
773
+ }, {
774
+ name: RGHistoryResources.SSR_MOTION_VECTOR,
775
+ handle: motionVectorHistoryHandle
776
+ });
666
777
  }
667
778
  }
668
- const includeSSRSurfaceMRT = !!options.ssr;
669
- // SSR glossy-surface MRT outputs (roughness + world normal) are graph
670
- // textures owned by this pass; effects reach them through the blackboard
671
- // handles (or the ctx fields resolved below during execution).
672
- const ssrRoughnessHandle = includeSSRSurfaceMRT ? builder.createTexture({
673
- format: getSurfaceTextureFormat(ctx),
674
- label: 'ssrRoughness'
675
- }) : undefined;
676
- const ssrNormalHandle = includeSSRSurfaceMRT ? builder.createTexture({
677
- format: getSurfaceTextureFormat(ctx),
678
- label: 'ssrNormal'
679
- }) : undefined;
680
- const writeSSSDiffuse = options.sss && shouldStoreSSSDiffuse(ctx);
681
- let writeSSSTransmission = options.sss && shouldStoreSSSTransmission(ctx);
682
- if (writeSSSDiffuse && writeSSSTransmission && includeSSRSurfaceMRT && getSSSLightingTextureFormat(ctx, 2, includeSSRSurfaceMRT) !== ctx.colorFormat) {
683
- writeSSSTransmission = false;
779
+ if (historyManager && options.ssgi) {
780
+ const ssgiHistorySize = {
781
+ width: ctx.renderWidth,
782
+ height: ctx.renderHeight
783
+ };
784
+ const irradianceHistoryHandle = historyManager.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_IRRADIANCE, {
785
+ format: 'rgba16f',
786
+ sizeMode: 'absolute',
787
+ width: ctx.renderWidth,
788
+ height: ctx.renderHeight
789
+ }, ssgiHistorySize);
790
+ const surfaceHistoryHandle = historyManager.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_SURFACE, {
791
+ format: 'rgba16f',
792
+ sizeMode: 'absolute',
793
+ width: ctx.renderWidth,
794
+ height: ctx.renderHeight
795
+ }, ssgiHistorySize);
796
+ if (irradianceHistoryHandle && surfaceHistoryHandle) {
797
+ lightHistoryReadBindings.push({
798
+ name: RGHistoryResources.SSGI_IRRADIANCE,
799
+ handle: irradianceHistoryHandle
800
+ }, {
801
+ name: RGHistoryResources.SSGI_SURFACE,
802
+ handle: surfaceHistoryHandle
803
+ });
804
+ }
684
805
  }
685
- const writeSkinSSS = options.skinSSS;
686
- const sssLightingAttachmentCount = (writeSSSDiffuse ? 1 : 0) + (writeSSSTransmission ? 1 : 0) + (writeSkinSSS ? 1 : 0);
687
- const sssLightingFormat = getSSSLightingTextureFormat(ctx, sssLightingAttachmentCount, includeSSRSurfaceMRT);
688
- const sssDiffuseHandle = writeSSSDiffuse ? builder.createTexture({
689
- format: sssLightingFormat,
690
- label: 'sssDiffuse'
691
- }) : undefined;
692
- const sssTransmissionHandle = writeSSSTransmission ? builder.createTexture({
693
- format: sssLightingFormat,
694
- label: 'sssTransmission'
695
- }) : undefined;
696
- const skinSSSHandle = writeSkinSSS ? builder.createTexture({
697
- format: sssLightingFormat,
698
- label: 'skinSSS'
699
- }) : undefined;
700
- const sceneColorFramebufferHandle = useFinalFramebufferAsIntermediate ? undefined : builder.createFramebuffer({
701
- label: 'SceneColorFramebuffer',
702
- width: ctx.renderWidth,
703
- height: ctx.renderHeight,
704
- colorAttachments: sceneColorHandle,
705
- depthAttachment: renderDepthAttachment
706
- });
707
- builder.setExecute((rgCtx)=>{
708
- const sceneColorTex = rgCtx.getTexture(sceneColorHandle);
709
- const sceneColorCopyTex = sceneColorCopyHandle ? rgCtx.getTexture(sceneColorCopyHandle) : null;
710
- // Resolve MRT products into the DrawContext bridge fields that scene
711
- // rendering and apply()-based effects still read.
712
- ctx.SSRRoughnessTexture = ssrRoughnessHandle ? rgCtx.getTexture(ssrRoughnessHandle) : null;
713
- ctx.SSRNormalTexture = ssrNormalHandle ? rgCtx.getTexture(ssrNormalHandle) : null;
714
- if (sssProfileResult) {
715
- ctx.SSSProfileTexture = rgCtx.getTexture(sssProfileResult.profileHandle);
716
- ctx.SSSParamTexture = rgCtx.getTexture(sssProfileResult.paramHandle);
717
- if (sssProfileResult.normalHandle) {
718
- ctx.SSRNormalTexture = rgCtx.getTexture(sssProfileResult.normalHandle);
719
- }
806
+ const opaquePassResult = graph.addPass('LightPass', (builder)=>{
807
+ builder.read(blackboard.expect(FrameResources.LinearDepth));
808
+ builder.read(depthPassResult.depthFramebufferHandle);
809
+ if (shadowMaskHandle) {
810
+ builder.read(shadowMaskHandle);
811
+ }
812
+ if (preLightTransmissionDepthToken) {
813
+ builder.read(preLightTransmissionDepthToken);
814
+ }
815
+ if (hiZHandle) {
816
+ builder.read(hiZHandle);
817
+ }
818
+ for (const binding of lightHistoryReadBindings){
819
+ builder.read(binding.handle);
820
+ }
821
+ // Direct rendering writes the backbuffer; otherwise create a texture.
822
+ const sceneColorHandle = useFinalFramebufferAsIntermediate ? builder.write(backbuffer) : builder.createTexture({
823
+ format: ctx.colorFormat,
824
+ label: 'sceneColor',
825
+ allocationKey: 'ForwardPlus.SceneColor'
826
+ });
827
+ if (sceneColorCopyHandle) {
828
+ builder.read(sceneColorCopyHandle);
829
+ }
830
+ if (sssProfileHandle) {
831
+ builder.read(sssProfileHandle);
720
832
  }
721
- ctx.SSSDiffuseTexture = sssDiffuseHandle ? rgCtx.getTexture(sssDiffuseHandle) : null;
722
- ctx.SSSTransmissionTexture = sssTransmissionHandle ? rgCtx.getTexture(sssTransmissionHandle) : null;
723
- ctx.SkinSSSTexture = skinSSSHandle ? rgCtx.getTexture(skinSSSHandle) : null;
724
- const renderLightPass = ()=>renderOpaqueScenePass(frame, sceneColorTex, sceneColorCopyTex, rgCtx, sceneColorFramebufferHandle);
725
- if (historyManager && lightHistoryReadBindings.length > 0) {
726
- historyManager.beginReadScope(lightHistoryReadBindings.map((binding)=>({
727
- name: binding.name,
728
- texture: rgCtx.getTexture(binding.handle)
729
- })));
730
- try {
833
+ if (sssParamHandle) {
834
+ builder.read(sssParamHandle);
835
+ }
836
+ const surfaceAttachmentCount = Number(options.sceneRoughness) + Number(options.sceneNormal);
837
+ // Surface MRT products are exposed through blackboard handles.
838
+ const sceneRoughnessHandle = options.sceneRoughness ? builder.createTexture({
839
+ format: getSurfaceTextureFormat(ctx),
840
+ label: 'sceneRoughness',
841
+ allocationKey: 'ForwardPlus.SceneRoughness'
842
+ }) : undefined;
843
+ const sceneNormalHandle = options.sceneNormal ? builder.createTexture({
844
+ format: getSurfaceTextureFormat(ctx),
845
+ label: 'sceneNormal',
846
+ allocationKey: 'ForwardPlus.SceneNormal'
847
+ }) : undefined;
848
+ const writeSSSDiffuse = options.sss && shouldStoreSSSDiffuse(ctx);
849
+ let writeSSSTransmission = options.sss && shouldStoreSSSTransmission(ctx);
850
+ if (writeSSSDiffuse && writeSSSTransmission && surfaceAttachmentCount > 0 && getSSSLightingTextureFormat(ctx, 2, surfaceAttachmentCount) !== ctx.colorFormat) {
851
+ writeSSSTransmission = false;
852
+ }
853
+ const writeSkinSSS = options.skinSSS;
854
+ const sssLightingAttachmentCount = (writeSSSDiffuse ? 1 : 0) + (writeSSSTransmission ? 1 : 0) + (writeSkinSSS ? 1 : 0);
855
+ const sssLightingFormat = getSSSLightingTextureFormat(ctx, sssLightingAttachmentCount, surfaceAttachmentCount);
856
+ const sssDiffuseHandle = writeSSSDiffuse ? builder.createTexture({
857
+ format: sssLightingFormat,
858
+ label: 'sssDiffuse',
859
+ allocationKey: 'ForwardPlus.SSSDiffuse'
860
+ }) : undefined;
861
+ const sssTransmissionHandle = writeSSSTransmission ? builder.createTexture({
862
+ format: sssLightingFormat,
863
+ label: 'sssTransmission',
864
+ allocationKey: 'ForwardPlus.SSSTransmission'
865
+ }) : undefined;
866
+ const skinSSSHandle = writeSkinSSS ? builder.createTexture({
867
+ format: sssLightingFormat,
868
+ label: 'skinSSS',
869
+ allocationKey: 'ForwardPlus.SkinSSS'
870
+ }) : undefined;
871
+ const sceneColorFramebufferHandle = useFinalFramebufferAsIntermediate ? undefined : builder.createFramebuffer({
872
+ label: 'SceneColorFramebuffer',
873
+ width: ctx.renderWidth,
874
+ height: ctx.renderHeight,
875
+ colorAttachments: sceneColorHandle,
876
+ depthAttachment: renderDepthAttachment
877
+ });
878
+ builder.setExecute((rgCtx)=>{
879
+ const sceneColorTex = rgCtx.getTexture(sceneColorHandle);
880
+ const sceneColorCopyTex = sceneColorCopyHandle ? rgCtx.getTexture(sceneColorCopyHandle) : null;
881
+ // Legacy effects still read resolved MRT textures from DrawContext.
882
+ ctx.shadowMaskTexture = shadowMaskHandle ? rgCtx.getTexture(shadowMaskHandle) : null;
883
+ ctx.HiZTexture = hiZHandle ? rgCtx.getTexture(hiZHandle) : null;
884
+ ctx.SceneRoughnessTexture = sceneRoughnessHandle ? rgCtx.getTexture(sceneRoughnessHandle) : null;
885
+ ctx.SceneNormalTexture = sceneNormalHandle ? rgCtx.getTexture(sceneNormalHandle) : null;
886
+ ctx.SSSProfileTexture = sssProfileHandle ? rgCtx.getTexture(sssProfileHandle) : null;
887
+ ctx.SSSParamTexture = sssParamHandle ? rgCtx.getTexture(sssParamHandle) : null;
888
+ ctx.SSSDiffuseTexture = sssDiffuseHandle ? rgCtx.getTexture(sssDiffuseHandle) : null;
889
+ ctx.SSSTransmissionTexture = sssTransmissionHandle ? rgCtx.getTexture(sssTransmissionHandle) : null;
890
+ ctx.SkinSSSTexture = skinSSSHandle ? rgCtx.getTexture(skinSSSHandle) : null;
891
+ const renderLightPass = ()=>renderOpaqueScenePass(frame, sceneColorTex, sceneColorCopyTex, rgCtx, sceneColorFramebufferHandle);
892
+ if (historyManager && lightHistoryReadBindings.length > 0) {
893
+ const ssgiIrradianceBinding = lightHistoryReadBindings.find((binding)=>binding.name === RGHistoryResources.SSGI_IRRADIANCE);
894
+ const ssgiSurfaceBinding = lightHistoryReadBindings.find((binding)=>binding.name === RGHistoryResources.SSGI_SURFACE);
895
+ ctx.SSGIIrradianceHistoryTexture = ssgiIrradianceBinding ? rgCtx.getTexture(ssgiIrradianceBinding.handle) : null;
896
+ ctx.SSGISurfaceHistoryTexture = ssgiSurfaceBinding ? rgCtx.getTexture(ssgiSurfaceBinding.handle) : null;
897
+ historyManager.beginReadScope(lightHistoryReadBindings.map((binding)=>({
898
+ name: binding.name,
899
+ texture: rgCtx.getTexture(binding.handle)
900
+ })));
901
+ try {
902
+ renderLightPass();
903
+ } finally{
904
+ historyManager.endReadScope();
905
+ ctx.SSGIIrradianceHistoryTexture = null;
906
+ ctx.SSGISurfaceHistoryTexture = null;
907
+ }
908
+ } else {
731
909
  renderLightPass();
732
- } finally{
733
- historyManager.endReadScope();
734
910
  }
735
- } else {
736
- renderLightPass();
737
- }
911
+ });
912
+ return {
913
+ sceneColorHandle,
914
+ sceneColorFramebufferHandle,
915
+ sceneRoughnessHandle,
916
+ sceneNormalHandle,
917
+ sssDiffuseHandle,
918
+ sssTransmissionHandle,
919
+ skinSSSHandle
920
+ };
738
921
  });
739
- return {
740
- sceneColorHandle,
741
- sceneColorCopyHandle,
742
- sceneColorFramebufferHandle,
743
- ssrRoughnessHandle,
744
- ssrNormalHandle,
745
- sssDiffuseHandle,
746
- sssTransmissionHandle,
747
- skinSSSHandle
748
- };
749
- });
750
- // Register the LightPass MRT products so effects can look them up by name.
751
- if (lightPassResult.ssrRoughnessHandle) {
752
- blackboard.set(FrameResources.SSRRoughness, lightPassResult.ssrRoughnessHandle);
753
- }
754
- if (lightPassResult.ssrNormalHandle) {
755
- blackboard.set(FrameResources.SSRNormal, lightPassResult.ssrNormalHandle);
756
- } else if (sssProfileResult?.normalHandle) {
757
- blackboard.set(FrameResources.SSRNormal, sssProfileResult.normalHandle);
758
- }
759
- if (lightPassResult.sssDiffuseHandle) {
760
- blackboard.set(FrameResources.SSSDiffuse, lightPassResult.sssDiffuseHandle);
761
- }
762
- if (lightPassResult.sssTransmissionHandle) {
763
- blackboard.set(FrameResources.SSSTransmission, lightPassResult.sssTransmissionHandle);
764
- }
765
- if (lightPassResult.skinSSSHandle) {
766
- blackboard.set(FrameResources.SkinSSS, lightPassResult.skinSSSHandle);
767
- }
768
- // 7d. Opaque-layer post effects (SAO/SSR/SSS/SkinSSS). They read the opaque
769
- // scene color and must complete before transparent geometry renders on top
770
- // of their output. Never direct-write: the chain output becomes the
771
- // transparent pass's render target. In final-framebuffer-as-intermediate
772
- // mode the scene color handle is the LightPass's backbuffer write version,
773
- // so the data flow is real either way — no keep-alive reads needed.
774
- const opaqueChainInput = lightPassResult.sceneColorHandle;
775
- // Data dependencies for every effect pass: frame textures the effects sample
776
- // through DrawContext fields (linear depth, HiZ, scene color copy, SSS MRT
777
- // outputs) rather than through declared require* hooks.
778
- const opaqueChainDeps = [
779
- depthHandle
780
- ];
781
- if (hiZHandle) {
782
- opaqueChainDeps.push(hiZHandle);
783
- }
784
- if (lightPassResult.sceneColorFramebufferHandle) {
785
- opaqueChainDeps.push(lightPassResult.sceneColorFramebufferHandle);
786
- }
787
- if (grabResult) {
788
- opaqueChainDeps.push(grabResult.copyHandle);
789
- }
790
- if (sssProfileResult) {
791
- opaqueChainDeps.push(sssProfileResult.profileHandle, sssProfileResult.paramHandle);
792
- if (sssProfileResult.normalHandle) {
793
- opaqueChainDeps.push(sssProfileResult.normalHandle);
922
+ const lightPassResult = opaquePassResult;
923
+ fg.state.lightPass = lightPassResult;
924
+ blackboard.set(FrameResources.SceneColor, lightPassResult.sceneColorHandle);
925
+ if (lightPassResult.sceneRoughnessHandle) {
926
+ blackboard.set(FrameResources.SceneRoughness, lightPassResult.sceneRoughnessHandle);
794
927
  }
795
- }
796
- for (const handle of [
797
- lightPassResult.ssrRoughnessHandle,
798
- lightPassResult.ssrNormalHandle,
799
- lightPassResult.sssDiffuseHandle,
800
- lightPassResult.sssTransmissionHandle,
801
- lightPassResult.skinSSSHandle
802
- ]){
803
- if (handle) {
804
- opaqueChainDeps.push(handle);
928
+ if (lightPassResult.sceneNormalHandle) {
929
+ blackboard.set(FrameResources.SceneNormal, lightPassResult.sceneNormalHandle);
805
930
  }
806
- }
807
- const opaqueChainResult = ctx.compositor ? ctx.compositor.buildLayer({
808
- graph,
809
- ctx,
810
- layer: PostEffectLayer.opaque,
811
- blackboard,
812
- input: opaqueChainInput,
813
- finalOutput: null,
814
- sceneDepthAttachment: renderDepthAttachment,
815
- dependencies: opaqueChainDeps,
816
- historyReads: lightHistoryReadBindings,
817
- history: historyManager
818
- }) : {
819
- color: opaqueChainInput,
820
- wroteFinal: false
821
- };
822
- const opaqueChainRan = opaqueChainResult.color !== opaqueChainInput;
823
- // 7e. Transparent scene geometry (transmission/transparent lists + OIT).
824
- // Renders on top of the opaque-chain output; graph-wise an in-place write
825
- // producing a new version of the current scene color.
826
- const sceneColorHandle = graph.addPass('TransparentPass', (builder)=>{
827
- builder.read(depthHandle);
828
- builder.read(depthPassResult.depthFramebufferHandle);
829
- if (hiZHandle) {
830
- // Transparent-phase materials may ray-march HiZ (e.g. water SSR)
831
- builder.read(hiZHandle);
931
+ if (lightPassResult.sssDiffuseHandle) {
932
+ blackboard.set(FrameResources.SSSDiffuse, lightPassResult.sssDiffuseHandle);
832
933
  }
833
- if (lightPassResult.sceneColorFramebufferHandle) {
834
- builder.read(lightPassResult.sceneColorFramebufferHandle);
934
+ if (lightPassResult.sssTransmissionHandle) {
935
+ blackboard.set(FrameResources.SSSTransmission, lightPassResult.sssTransmissionHandle);
835
936
  }
836
- if (lightPassResult.sceneColorCopyHandle) {
837
- builder.read(lightPassResult.sceneColorCopyHandle);
937
+ if (lightPassResult.skinSSSHandle) {
938
+ blackboard.set(FrameResources.SkinSSS, lightPassResult.skinSSSHandle);
838
939
  }
839
- builder.read(opaqueChainResult.color);
840
- const out = builder.write(opaqueChainResult.color);
841
- builder.setExecute((rgCtx)=>{
842
- renderTransparentScenePass(frame, rgCtx, opaqueChainRan ? opaqueChainResult.color : null, lightPassResult.sceneColorFramebufferHandle);
843
- });
844
- return out;
845
- });
846
- blackboard.set(FrameResources.SceneColor, sceneColorHandle);
847
- if (lightPassResult.sceneColorCopyHandle) {
848
- blackboard.set(FrameResources.SceneColorCopy, lightPassResult.sceneColorCopyHandle);
849
940
  }
850
- if (sssProfileResult) {
851
- blackboard.set(FrameResources.SSSProfile, sssProfileResult.profileHandle);
852
- blackboard.set(FrameResources.SSSParam, sssProfileResult.paramHandle);
853
- }
854
- // 8. Post effect chains + transmission depth.
855
- //
856
- // Chain input: the TransparentPass output version — the authoritative scene
857
- // color regardless of whether it physically lives in a texture or in the
858
- // backbuffer (final framebuffer used as intermediate, no opaque effects).
859
- const chainInput = sceneColorHandle;
860
- // When the scene color still physically resides in the final framebuffer,
861
- // the Present blit must be skipped if no effect moved it to a texture.
862
- // (Opaque-layer effects disable final-as-intermediate mode, so the scene
863
- // color is backbuffer-resident whenever that mode is active.)
864
- const backbufferResidentHandle = useFinalFramebufferAsIntermediate ? sceneColorHandle : null;
865
- // No extra chain dependencies: the effect chains link to the scene color
866
- // through their inputs, and per-effect texture needs are declared via the
867
- // require* hooks in AbstractPostEffect.setup().
868
- const chainDependencies = [];
869
- const finalOutput = {
870
- handle: backbuffer,
871
- isScreen: !ctx.finalFramebuffer
872
- };
873
- const endLayerHasEffects = !!ctx.compositor?.layerHasEnabledEffect(PostEffectLayer.end);
874
- // 8a. Transparent-layer effects (bloom, tonemap, FXAA, ...). They run right
875
- // after the light pass and must sample the pre-transmission linear depth, so
876
- // they carry no transmissionDepthToken dependency; TransmissionDepth is
877
- // instead ordered after this chain (see 8b).
878
- const transparentChainResult = ctx.compositor ? ctx.compositor.buildLayer({
879
- graph,
880
- ctx,
881
- layer: PostEffectLayer.transparent,
882
- blackboard,
883
- input: chainInput,
884
- finalOutput: endLayerHasEffects ? null : finalOutput,
885
- inputResidesInFinalTarget: !!backbufferResidentHandle,
886
- sceneDepthAttachment: renderDepthAttachment,
887
- dependencies: chainDependencies,
888
- history: historyManager
889
- }) : {
890
- color: chainInput,
891
- wroteFinal: false
892
- };
893
- // 8b. Transmission depth pass (optional). Mutates the linear depth texture;
894
- // the mutation is modeled as a graph write: the WAR hazard orders this pass
895
- // after every pre-transmission depth reader (the transparent-layer chain),
896
- // and re-registering the post-write version in the blackboard gives
897
- // end-layer effects (TAA) a real data dependency on the transmission depth.
898
- let transmissionDepthToken;
899
- if (options.needSceneColor && !options.needsTransmissionDepthForSSR) {
900
- const transmissionDepthResult = graph.addPass('TransmissionDepth', (builder)=>{
941
+ };
942
+ /** @internal */ const SkyPassModule = {
943
+ type: 'SkyPass',
944
+ reads: [
945
+ {
946
+ resource: FrameResources.SceneColor,
947
+ version: 'current'
948
+ }
949
+ ],
950
+ writes: [
951
+ FrameResources.SceneColor,
952
+ FrameResources.SceneColorNoFog
953
+ ],
954
+ prepare: ()=>({
955
+ enabled: true
956
+ }),
957
+ setup (fg) {
958
+ const { graph, ctx, frame, blackboard, options } = fg;
959
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SkyPass');
960
+ const lightPassResult = requireBuildState(fg, 'lightPass', 'LightPass', 'SkyPass');
961
+ const sceneColorCopyHandle = blackboard.get(FrameResources.SceneColorCopy);
962
+ const sceneColorHandle = blackboard.expect(FrameResources.SceneColor);
963
+ // Screen-space passes must not read fog as surface radiance. Snapshot the lit
964
+ // scene with sky but before fog, and only when fog is actually present and a
965
+ // consumer exists, so fog-free scenes allocate nothing extra. The refraction
966
+ // background path already baked sky and fog into the copy, so no fog-free
967
+ // version can be recovered there and consumers keep their existing input.
968
+ const captureNoFog = !sceneColorCopyHandle && (options.ssgi || options.ssr) && options.fogPresents;
969
+ const skyPassResult = graph.addPass('SkyPass', (builder)=>{
901
970
  builder.read(sceneColorHandle);
902
- if (transparentChainResult.color !== sceneColorHandle) {
903
- builder.read(transparentChainResult.color);
904
- }
905
- builder.read(depthHandle);
906
971
  builder.read(depthPassResult.depthFramebufferHandle);
907
- const depthOut = builder.write(depthHandle);
908
- const done = builder.createToken('TransmissionDepthDone');
909
- builder.sideEffect();
972
+ if (lightPassResult.sceneColorFramebufferHandle) {
973
+ builder.read(lightPassResult.sceneColorFramebufferHandle);
974
+ }
975
+ const noFog = captureNoFog ? builder.createTexture({
976
+ format: ctx.colorFormat,
977
+ label: 'sceneColorNoFog',
978
+ allocationKey: 'ForwardPlus.SceneColorNoFog'
979
+ }) : null;
980
+ const out = builder.write(sceneColorHandle);
910
981
  builder.setExecute((rgCtx)=>{
911
- renderTransmissionDepthPass(frame, rgCtx);
982
+ // The refraction background already contains sky and fog.
983
+ if (!sceneColorCopyHandle) {
984
+ renderSkyScenePass(frame, rgCtx, lightPassResult.sceneColorFramebufferHandle, noFog ? rgCtx.getTexture(noFog) : null);
985
+ }
912
986
  });
913
987
  return {
914
- done,
915
- depthOut
988
+ color: out,
989
+ noFog
916
990
  };
917
991
  });
918
- transmissionDepthToken = transmissionDepthResult.done;
919
- depthHandle = transmissionDepthResult.depthOut;
920
- // The transparent-layer chain above read the pre-transmission version;
921
- // everything built from here on (end-layer chain) reads this one.
922
- blackboard.set(FrameResources.LinearDepth, depthHandle);
992
+ lightPassResult.sceneColorHandle = skyPassResult.color;
993
+ blackboard.set(FrameResources.SceneColor, skyPassResult.color);
994
+ if (skyPassResult.noFog) {
995
+ blackboard.set(FrameResources.SceneColorNoFog, skyPassResult.noFog);
996
+ }
923
997
  }
924
- // 9. End-layer effects (TAA). Ordered after TransmissionDepth.
925
- const endChainDependencies = transmissionDepthToken ? [
926
- ...chainDependencies,
927
- transmissionDepthToken
928
- ] : chainDependencies;
929
- const chainResult = ctx.compositor ? ctx.compositor.buildLayer({
930
- graph,
931
- ctx,
932
- layer: PostEffectLayer.end,
933
- blackboard,
934
- input: transparentChainResult.color,
935
- finalOutput,
936
- // Still backbuffer-resident if the transparent-layer chain ran no effect
937
- inputResidesInFinalTarget: !!backbufferResidentHandle && transparentChainResult.color === chainInput,
938
- sceneDepthAttachment: renderDepthAttachment,
939
- dependencies: endChainDependencies,
940
- history: historyManager
941
- }) : {
942
- color: transparentChainResult.color,
943
- wroteFinal: false
944
- };
945
- const finalWroteFinal = chainResult.wroteFinal || transparentChainResult.wroteFinal;
946
- // 10. Present + frame cleanup.
947
- let presentedBackbuffer;
948
- if (finalWroteFinal) {
949
- // The last effect wrote the final target directly; only cleanup remains.
950
- presentedBackbuffer = chainResult.color;
951
- graph.addPass('FrameCleanup', (builder)=>{
952
- builder.read(presentedBackbuffer);
953
- for (const dep of endChainDependencies){
954
- builder.read(dep);
998
+ };
999
+ /** @internal */ const CompositeTailModule = {
1000
+ type: 'CompositeTail',
1001
+ writes: [
1002
+ FrameResources.SceneColor,
1003
+ FrameResources.LinearDepth,
1004
+ FrameResources.PresentedColor
1005
+ ],
1006
+ prepare: ()=>({
1007
+ enabled: true
1008
+ }),
1009
+ setup (fg) {
1010
+ const { graph, ctx, frame, blackboard, options, backbuffer } = fg;
1011
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'CompositeTail');
1012
+ const hiZHandle = blackboard.get(FrameResources.HiZ);
1013
+ const sceneColorCopyHandle = blackboard.get(FrameResources.SceneColorCopy);
1014
+ const lightPassResult = requireBuildState(fg, 'lightPass', 'LightPass', 'CompositeTail');
1015
+ const renderDepthAttachment = fg.state.renderDepthAttachment;
1016
+ const useFinalFramebufferAsIntermediate = fg.state.useFinalFramebufferAsIntermediate;
1017
+ const lightHistoryReadBindings = fg.state.lightHistoryReadBindings;
1018
+ const historyManager = fg.history;
1019
+ // Opaque effects finish before transparent geometry uses their output.
1020
+ const opaqueChainInput = blackboard.expect(FrameResources.SceneColor);
1021
+ // Include textures sampled indirectly through DrawContext.
1022
+ const opaqueChainDeps = [
1023
+ blackboard.expect(FrameResources.LinearDepth)
1024
+ ];
1025
+ if (hiZHandle) {
1026
+ opaqueChainDeps.push(hiZHandle);
1027
+ }
1028
+ if (lightPassResult.sceneColorFramebufferHandle) {
1029
+ opaqueChainDeps.push(lightPassResult.sceneColorFramebufferHandle);
1030
+ }
1031
+ if (sceneColorCopyHandle) {
1032
+ opaqueChainDeps.push(sceneColorCopyHandle);
1033
+ }
1034
+ for (const handle of [
1035
+ blackboard.get(FrameResources.SSSProfile),
1036
+ blackboard.get(FrameResources.SSSParam),
1037
+ blackboard.get(FrameResources.SceneRoughness),
1038
+ blackboard.get(FrameResources.SceneNormal),
1039
+ blackboard.get(FrameResources.SSSDiffuse),
1040
+ blackboard.get(FrameResources.SSSTransmission),
1041
+ blackboard.get(FrameResources.SkinSSS)
1042
+ ]){
1043
+ if (handle) {
1044
+ opaqueChainDeps.push(handle);
955
1045
  }
956
- builder.sideEffect();
957
- builder.setExecute(()=>{
958
- finishFrame(frame);
959
- });
960
- });
961
- } else {
962
- presentedBackbuffer = graph.addPass('Present', (builder)=>{
963
- builder.read(chainResult.color);
964
- for (const dep of endChainDependencies){
965
- builder.read(dep);
1046
+ }
1047
+ const opaqueChainResult = ctx.compositor ? ctx.compositor.buildLayer({
1048
+ graph,
1049
+ ctx,
1050
+ layer: PostEffectLayer.opaque,
1051
+ blackboard,
1052
+ input: opaqueChainInput,
1053
+ finalOutput: null,
1054
+ sceneDepthAttachment: renderDepthAttachment,
1055
+ dependencies: opaqueChainDeps,
1056
+ historyReads: lightHistoryReadBindings,
1057
+ history: historyManager
1058
+ }) : {
1059
+ color: opaqueChainInput,
1060
+ wroteFinal: false
1061
+ };
1062
+ const opaqueChainRan = opaqueChainResult.color !== opaqueChainInput;
1063
+ // Transparent geometry writes a new version of the opaque-chain output.
1064
+ const sceneColorHandle = graph.addPass('TransparentPass', (builder)=>{
1065
+ builder.read(blackboard.expect(FrameResources.LinearDepth));
1066
+ builder.read(depthPassResult.depthFramebufferHandle);
1067
+ if (hiZHandle) {
1068
+ // Transparent materials may ray-march HiZ.
1069
+ builder.read(hiZHandle);
1070
+ }
1071
+ if (lightPassResult.sceneColorFramebufferHandle) {
1072
+ builder.read(lightPassResult.sceneColorFramebufferHandle);
966
1073
  }
967
- const outputBackbuffer = builder.write(backbuffer);
968
- // Skip the blit when the chain output already lives in the final target
969
- // (final framebuffer used as intermediate and no end-layer effect ran).
970
- const needsBlit = chainResult.color !== backbufferResidentHandle;
1074
+ if (sceneColorCopyHandle) {
1075
+ builder.read(sceneColorCopyHandle);
1076
+ }
1077
+ builder.read(opaqueChainResult.color);
1078
+ const out = builder.write(opaqueChainResult.color);
971
1079
  builder.setExecute((rgCtx)=>{
972
- const sourceTex = needsBlit ? rgCtx.getTexture(chainResult.color) : null;
973
- if (sourceTex) {
974
- const blitter = new CopyBlitter();
975
- blitter.srgbOut = !ctx.finalFramebuffer;
976
- blitter.blit(sourceTex, ctx.finalFramebuffer ?? null, fetchSampler('clamp_nearest_nomip'));
977
- }
978
- finishFrame(frame);
1080
+ renderTransparentScenePass(frame, rgCtx, opaqueChainRan ? opaqueChainResult.color : null, lightPassResult.sceneColorFramebufferHandle);
979
1081
  });
980
- return outputBackbuffer;
1082
+ return out;
981
1083
  });
1084
+ blackboard.set(FrameResources.SceneColor, sceneColorHandle);
1085
+ const chainInput = sceneColorHandle;
1086
+ // Track whether the chain input still resides in the final target.
1087
+ const backbufferResidentHandle = useFinalFramebufferAsIntermediate ? sceneColorHandle : null;
1088
+ const chainDependencies = [];
1089
+ const finalOutput = {
1090
+ handle: backbuffer,
1091
+ isScreen: !ctx.finalFramebuffer
1092
+ };
1093
+ const endLayerHasEffects = !!ctx.compositor?.layerHasEnabledEffect(PostEffectLayer.end);
1094
+ // Transparent-layer effects read depth before its transmission mutation.
1095
+ const transparentChainResult = ctx.compositor ? ctx.compositor.buildLayer({
1096
+ graph,
1097
+ ctx,
1098
+ layer: PostEffectLayer.transparent,
1099
+ blackboard,
1100
+ input: chainInput,
1101
+ finalOutput: endLayerHasEffects ? null : finalOutput,
1102
+ inputResidesInFinalTarget: !!backbufferResidentHandle,
1103
+ sceneDepthAttachment: renderDepthAttachment,
1104
+ dependencies: chainDependencies,
1105
+ history: historyManager
1106
+ }) : {
1107
+ color: chainInput,
1108
+ wroteFinal: false
1109
+ };
1110
+ // WAR orders this depth mutation after pre-transmission readers.
1111
+ let transmissionDepthToken;
1112
+ if (options.needSceneColor && !options.needsTransmissionDepthForSSR) {
1113
+ const transmissionDepthResult = graph.addPass('TransmissionDepth', (builder)=>{
1114
+ const currentDepth = blackboard.expect(FrameResources.LinearDepth);
1115
+ builder.read(sceneColorHandle);
1116
+ if (transparentChainResult.color !== sceneColorHandle) {
1117
+ builder.read(transparentChainResult.color);
1118
+ }
1119
+ builder.read(currentDepth);
1120
+ builder.read(depthPassResult.depthFramebufferHandle);
1121
+ const depthOut = builder.write(currentDepth);
1122
+ const done = builder.createToken('TransmissionDepthDone');
1123
+ builder.sideEffect();
1124
+ builder.setExecute((rgCtx)=>{
1125
+ renderTransmissionDepthPass(frame, rgCtx);
1126
+ });
1127
+ return {
1128
+ done,
1129
+ depthOut
1130
+ };
1131
+ });
1132
+ transmissionDepthToken = transmissionDepthResult.done;
1133
+ blackboard.set(FrameResources.LinearDepth, transmissionDepthResult.depthOut);
1134
+ }
1135
+ const endChainDependencies = transmissionDepthToken ? [
1136
+ ...chainDependencies,
1137
+ transmissionDepthToken
1138
+ ] : chainDependencies;
1139
+ const chainResult = ctx.compositor ? ctx.compositor.buildLayer({
1140
+ graph,
1141
+ ctx,
1142
+ layer: PostEffectLayer.end,
1143
+ blackboard,
1144
+ input: transparentChainResult.color,
1145
+ finalOutput,
1146
+ inputResidesInFinalTarget: !!backbufferResidentHandle && transparentChainResult.color === chainInput,
1147
+ sceneDepthAttachment: renderDepthAttachment,
1148
+ dependencies: endChainDependencies,
1149
+ history: historyManager
1150
+ }) : {
1151
+ color: transparentChainResult.color,
1152
+ wroteFinal: false
1153
+ };
1154
+ const finalWroteFinal = chainResult.wroteFinal || transparentChainResult.wroteFinal;
1155
+ let presentedBackbuffer;
1156
+ if (finalWroteFinal) {
1157
+ presentedBackbuffer = chainResult.color;
1158
+ } else {
1159
+ presentedBackbuffer = graph.addPass('Blit', (builder)=>{
1160
+ builder.read(chainResult.color);
1161
+ for (const dep of endChainDependencies){
1162
+ builder.read(dep);
1163
+ }
1164
+ const outputBackbuffer = builder.write(backbuffer);
1165
+ // Skip copies when the output already resides in the final target.
1166
+ const needsBlit = chainResult.color !== backbufferResidentHandle;
1167
+ builder.setExecute((rgCtx)=>{
1168
+ const sourceTex = needsBlit ? rgCtx.getTexture(chainResult.color) : null;
1169
+ if (sourceTex) {
1170
+ const blitter = new CopyBlitter();
1171
+ blitter.srgbOut = !ctx.finalFramebuffer;
1172
+ blitter.blit(sourceTex, ctx.finalFramebuffer ?? null, fetchSampler('clamp_nearest_nomip'));
1173
+ }
1174
+ });
1175
+ return outputBackbuffer;
1176
+ });
1177
+ }
1178
+ fg.state.presentedBackbuffer = presentedBackbuffer;
1179
+ // Downstream modules may replace this sink registration.
1180
+ blackboard.set(FrameResources.PresentedColor, presentedBackbuffer);
982
1181
  }
1182
+ };
1183
+ /** Built-in Forward+ modules addressable by pipeline anchors. @public */ const ForwardPlusModules = {
1184
+ SkyUpdate: SkyUpdateModule,
1185
+ ClusterLights: ClusterLightsModule,
1186
+ GPUPicking: GPUPickingModule,
1187
+ ShadowMaps: ShadowMapsModule,
1188
+ DepthPrepass: DepthPrepassModule,
1189
+ ShadowMask: ShadowMaskModule,
1190
+ TransmissionDepthForSSR: TransmissionDepthForSSRModule,
1191
+ HiZ: HiZModule,
1192
+ SSSProfile: SSSProfileModule,
1193
+ SceneColorGrab: SceneColorGrabModule,
1194
+ LightPass: LightPassModule,
1195
+ SkyPass: SkyPassModule,
1196
+ CompositeTail: CompositeTailModule
1197
+ };
1198
+ /** The default module order assembled by {@link createForwardPlusPipeline}. */ const DEFAULT_FORWARD_PLUS_MODULES = [
1199
+ SkyUpdateModule,
1200
+ ClusterLightsModule,
1201
+ GPUPickingModule,
1202
+ ShadowMapsModule,
1203
+ DepthPrepassModule,
1204
+ ShadowMaskModule,
1205
+ TransmissionDepthForSSRModule,
1206
+ HiZModule,
1207
+ SSSProfileModule,
1208
+ SceneColorGrabModule,
1209
+ LightPassModule,
1210
+ SkyPassModule,
1211
+ CompositeTailModule
1212
+ ];
1213
+ /** @internal */ let _defaultForwardPlusPipeline = null;
1214
+ /** Create an independent pipeline with the built-in Forward+ modules. @public */ function createForwardPlusPipeline() {
1215
+ return new RenderPipeline(DEFAULT_FORWARD_PLUS_MODULES);
1216
+ }
1217
+ /** Return the shared default pipeline. Mutations affect cameras using it. @public */ function getDefaultForwardPlusPipeline() {
1218
+ if (!_defaultForwardPlusPipeline) {
1219
+ _defaultForwardPlusPipeline = createForwardPlusPipeline();
1220
+ }
1221
+ return _defaultForwardPlusPipeline;
1222
+ }
1223
+ /** Build the Forward+ render graph and return its backbuffer output. @public */ function buildForwardPlusGraph(graph, ctx, renderQueue, options) {
1224
+ return buildForwardPlusGraphInternal(graph, ctx, renderQueue, options).backbuffer;
1225
+ }
1226
+ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
1227
+ const backbuffer = graph.importTexture('backbuffer');
1228
+ // Irradiance, moments and surface history require renderable half-float
1229
+ // targets. Keep ordinary IBL active on devices that cannot provide them.
1230
+ options.ssgi &&= supportsSSGIRenderTargets(ctx);
1231
+ ctx.SSS = !!options.sss;
1232
+ ctx.SSGI = !!options.ssgi;
1233
+ ctx.SSGIIrradianceHistoryTexture = null;
1234
+ ctx.SSGISurfaceHistoryTexture = null;
1235
+ ctx.SkinSSSTexture = null;
1236
+ // ShadowMask sets this only when it produces a texture.
1237
+ ctx.shadowMaskTexture = null;
1238
+ const blackboard = new RGBlackboard();
1239
+ const frame = {
1240
+ ctx,
1241
+ renderQueue,
1242
+ depthFramebuffer: null,
1243
+ sunLightColor: null,
1244
+ options,
1245
+ renderQueueDisposed: false,
1246
+ clusteredLightReleased: false,
1247
+ sunLightRestored: false
1248
+ };
1249
+ const ordering = new OrderingScope();
1250
+ const fg = {
1251
+ graph,
1252
+ ctx,
1253
+ finalFramebuffer: ctx.finalFramebuffer,
1254
+ renderQueue,
1255
+ blackboard,
1256
+ frame,
1257
+ history: ctx.camera?.getHistoryResourceManager?.() ?? null,
1258
+ options,
1259
+ ordering,
1260
+ backbuffer,
1261
+ state: {
1262
+ renderDepthAttachment: null,
1263
+ useFinalFramebufferAsIntermediate: false,
1264
+ lightHistoryReadBindings: []
1265
+ }
1266
+ };
1267
+ const pipeline = ctx.camera?.renderPipeline ?? getDefaultForwardPlusPipeline();
1268
+ const requirements = ctx.compositor?.collectRequirements(ctx) ?? {};
1269
+ mergeFrameResourceRequirements(requirements, pipeline.collectRequirements(fg));
1270
+ resolveFrameResourceRequirements(ctx, options, requirements);
1271
+ pipeline.build(fg);
1272
+ validateProducedFrameResources(blackboard, options, renderQueue);
1273
+ const presented = blackboard.get(FrameResources.PresentedColor) ?? fg.state.presentedBackbuffer;
1274
+ const depth = fg.state.depth;
1275
+ const externalDepthImport = depth?.externalDepthAttachmentHandle && depth.externalDepthAttachment ? {
1276
+ handle: depth.externalDepthAttachmentHandle,
1277
+ texture: depth.externalDepthAttachment
1278
+ } : undefined;
983
1279
  return {
984
- backbuffer: presentedBackbuffer,
985
- frame
1280
+ backbuffer: presented,
1281
+ frame,
1282
+ externalDepthImport
986
1283
  };
987
1284
  }
988
- // ─── Pass Implementation Helpers ────────────────────────────────────
989
- // These wrap the existing SceneRenderer static methods, adapted to work
990
- // with the FrameState pattern. They contain the same logic as the
991
- // original methods but read/write through FrameState.
992
1285
  /** @internal */ function renderShadowMaps(ctx, lights) {
993
1286
  ctx.renderPass = _shadowMapPass;
994
1287
  ctx.device.pushDeviceStates();
@@ -1021,20 +1314,17 @@ function renderForwardSSSProfile(frame, profileFramebuffer, profileTexture, para
1021
1314
  const savedCommandBufferReuse = ctx.camera.commandBufferReuse;
1022
1315
  const savedProfileTexture = ctx.SSSProfileTexture;
1023
1316
  const savedParamTexture = ctx.SSSParamTexture;
1024
- const savedNormalTexture = ctx.SSRNormalTexture;
1317
+ const savedNormalTexture = ctx.SceneNormalTexture;
1025
1318
  let profileFlags = MaterialVaryingFlags.SSS_STORE_PROFILE;
1026
- if (normalTexture) {
1027
- profileFlags |= MaterialVaryingFlags.SSS_STORE_NORMAL;
1028
- }
1029
1319
  device.pushDeviceStates();
1030
1320
  try {
1031
1321
  device.setFramebuffer(profileFramebuffer);
1032
1322
  ctx.SSSProfileTexture = profileTexture;
1033
1323
  ctx.SSSParamTexture = paramTexture;
1034
- ctx.SSRNormalTexture = normalTexture;
1324
+ ctx.SceneNormalTexture = normalTexture;
1035
1325
  ctx.compositor = null;
1036
1326
  ctx.camera.commandBufferReuse = false;
1037
- ctx.materialFlags = ctx.materialFlags & ~(MaterialVaryingFlags.SSR_STORE_ROUGHNESS | MaterialVaryingFlags.SSS_STORE_PROFILE | MaterialVaryingFlags.SSS_STORE_NORMAL | MaterialVaryingFlags.SSS_STORE_DIFFUSE | MaterialVaryingFlags.SSS_STORE_TRANSMISSION | MaterialVaryingFlags.SKIN_SSS_STORE) | profileFlags;
1327
+ ctx.materialFlags = ctx.materialFlags & ~(MaterialVaryingFlags.SCENE_STORE_ROUGHNESS | MaterialVaryingFlags.SSS_STORE_PROFILE | MaterialVaryingFlags.SCENE_STORE_NORMAL | MaterialVaryingFlags.SSS_STORE_DIFFUSE | MaterialVaryingFlags.SSS_STORE_TRANSMISSION | MaterialVaryingFlags.SKIN_SSS_STORE) | profileFlags;
1038
1328
  _scenePass.transmission = false;
1039
1329
  _scenePass.renderOpaque = true;
1040
1330
  _scenePass.renderTransparent = false;
@@ -1054,14 +1344,13 @@ function renderForwardSSSProfile(frame, profileFramebuffer, profileTexture, para
1054
1344
  ctx.compositor = savedCompositor;
1055
1345
  ctx.SSSProfileTexture = savedProfileTexture;
1056
1346
  ctx.SSSParamTexture = savedParamTexture;
1057
- ctx.SSRNormalTexture = savedNormalTexture;
1347
+ ctx.SceneNormalTexture = savedNormalTexture;
1058
1348
  device.popDeviceStates();
1059
1349
  sssRenderQueue.dispose();
1060
1350
  }
1061
1351
  }
1062
1352
  function releaseIntermediateFramebuffer(frame) {
1063
- // Device state is contained within each pass now (LightPass/TransparentPass
1064
- // push/pop their own framebuffer bindings); only the context field remains.
1353
+ // Each pass owns device state; only clear the shared context field.
1065
1354
  frame.ctx.intermediateFramebuffer = null;
1066
1355
  }
1067
1356
  function releaseDepthFramebuffer(frame) {
@@ -1087,6 +1376,13 @@ function restoreSunLight(frame) {
1087
1376
  }
1088
1377
  }
1089
1378
  function cleanupFrame(frame) {
1379
+ const { ctx } = frame;
1380
+ ctx.materialFlags &= ~MaterialVaryingFlags.SCENE_STORE_ROUGHNESS;
1381
+ ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_PROFILE;
1382
+ ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_DIFFUSE;
1383
+ ctx.materialFlags &= ~MaterialVaryingFlags.SCENE_STORE_NORMAL;
1384
+ ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_TRANSMISSION;
1385
+ ctx.materialFlags &= ~MaterialVaryingFlags.SKIN_SSS_STORE;
1090
1386
  releaseIntermediateFramebuffer(frame);
1091
1387
  releaseDepthFramebuffer(frame);
1092
1388
  releaseClusteredLight(frame);
@@ -1098,10 +1394,10 @@ function cleanupFrame(frame) {
1098
1394
  const renderQueue = frame.renderQueue;
1099
1395
  const transmission = transmissionOverride ?? !!existingDepthFb;
1100
1396
  let depthFramebuffer = existingDepthFb;
1397
+ // Current callers must provide the graph-managed prepass framebuffer.
1398
+ console.assert(!!existingDepthFb, 'renderSceneDepth: called without an existing depth framebuffer; the ' + 'self-allocating fallback path is untested in the forward+ pipeline.');
1101
1399
  if (!depthFramebuffer) {
1102
- // Use RenderGraph-allocated textures if provided
1103
1400
  {
1104
- // Allocate through RenderGraph so framebuffer lifetime is owned by the executor.
1105
1401
  const format = ctx.device.type === 'webgl' ? ctx.SSRCalcThickness ? 'rgba16f' : 'rgba8unorm' : ctx.SSRCalcThickness ? 'rg32f' : 'r32f';
1106
1402
  const mvFormat = 'rgba16f';
1107
1403
  if (!ctx.finalFramebuffer) {
@@ -1151,7 +1447,7 @@ function cleanupFrame(frame) {
1151
1447
  ctx.device.setFramebuffer(depthFramebuffer);
1152
1448
  _depthPass.encodeDepth = depthFramebuffer.getColorAttachments()[0].format === 'rgba8unorm';
1153
1449
  _depthPass.clearColor = transmission ? null : _depthPass.encodeDepth ? new Vector4(0, 0, 0, 1) : new Vector4(1, 1, 1, 1);
1154
- _depthPass.clearDepth = transmission ? null : 1;
1450
+ _depthPass.clearDepth = transmission ? null : DEPTH_CLEAR_VALUE;
1155
1451
  _depthPass.clearStencil = null;
1156
1452
  _depthPass.transmission = transmission;
1157
1453
  if (ctx.SSRCalcThickness && !transmission) {
@@ -1183,11 +1479,9 @@ function cleanupFrame(frame) {
1183
1479
  ctx.linearDepthTexture = depthFramebuffer.getColorAttachments()[0];
1184
1480
  ctx.depthTexture = depthFramebuffer.getDepthAttachment();
1185
1481
  ctx.depthPrepassAttachment = ctx.depthTexture;
1186
- // HiZ is now built in the dedicated HiZ pass
1187
1482
  }
1188
1483
  return depthFramebuffer;
1189
1484
  }
1190
- // ─── Sky Motion Vector State ────────────────────────────────────────
1191
1485
  let _skyMVProgram = null;
1192
1486
  let _skyMVBindGroup = null;
1193
1487
  let _skyMVBox = null;
@@ -1213,7 +1507,7 @@ let _skyMVBox = null;
1213
1507
  this.$l.prevWorldPos = pb.add(this.$inputs.pos, this.prevCameraPos);
1214
1508
  this.$l.clipPos = pb.mul(this.VPMatrix, pb.vec4(this.worldPos, 1));
1215
1509
  this.$l.prevClipPos = pb.mul(this.prevVPMatrix, pb.vec4(this.prevWorldPos, 1));
1216
- this.clipPos.z = this.clipPos.w;
1510
+ this.clipPos.z = ShaderHelper.farthestClipZ(this, this.clipPos.w);
1217
1511
  this.$builtins.position = this.clipPos;
1218
1512
  this.$outputs.currentPos = this.clipPos;
1219
1513
  this.$outputs.prevPos = this.prevClipPos;
@@ -1246,7 +1540,7 @@ let _skyMVBox = null;
1246
1540
  device.pushDeviceStates();
1247
1541
  device.setProgram(_skyMVProgram);
1248
1542
  device.setBindGroup(0, _skyMVBindGroup);
1249
- device.setRenderStates(AbstractPostEffect.getDefaultRenderState(ctx, 'le'));
1543
+ device.setRenderStates(AbstractPostEffect.getDefaultRenderState(ctx, DEPTH_COMPARE_DEFAULT));
1250
1544
  device.setFramebuffer(fb);
1251
1545
  _skyMVBox.draw();
1252
1546
  device.popDeviceStates();
@@ -1256,11 +1550,7 @@ function blitToCurrentColorAttachment(ctx, source) {
1256
1550
  const destination = framebuffer?.getColorAttachment(0) ?? null;
1257
1551
  new CopyBlitter().blit(source, destination, fetchSampler('clamp_nearest_nomip'));
1258
1552
  }
1259
- /**
1260
- * Renders the full scene (no transmission) into the scene-color copy texture
1261
- * used as refraction background. Runs as its own graph pass before LightPass.
1262
- * @internal
1263
- */ function renderSceneColorGrab(frame, rgCtx, copyHandle, copyFramebufferHandle) {
1553
+ /** Render the refraction background before LightPass. @internal */ function renderSceneColorGrab(frame, rgCtx, copyHandle, copyFramebufferHandle) {
1264
1554
  const { ctx, renderQueue } = frame;
1265
1555
  const device = ctx.device;
1266
1556
  const depthTex = frame.depthFramebuffer?.getDepthAttachment();
@@ -1270,7 +1560,7 @@ function blitToCurrentColorAttachment(ctx, source) {
1270
1560
  const isolateSceneColorDepth = frame.options.needsTransmissionDepthForSSR;
1271
1561
  const savedDepthPrepassAttachment = ctx.depthPrepassAttachment;
1272
1562
  const savedMaterialFlags = ctx.materialFlags;
1273
- // MRT store flags never apply to the background copy
1563
+ // The background copy has no surface MRT attachments.
1274
1564
  const sceneColorMaterialFlags = ctx.materialFlags & ~SURFACE_MRT_FLAGS;
1275
1565
  const sceneColorFramebuffer = copyFramebufferHandle ? rgCtx.getFramebuffer(copyFramebufferHandle) : rgCtx.createFramebuffer({
1276
1566
  width: copyTex.width,
@@ -1287,10 +1577,10 @@ function blitToCurrentColorAttachment(ctx, source) {
1287
1577
  _scenePass.transmission = false;
1288
1578
  if (isolateSceneColorDepth) {
1289
1579
  ctx.depthPrepassAttachment = undefined;
1290
- _scenePass.clearDepth = 1;
1580
+ _scenePass.clearDepth = DEPTH_CLEAR_VALUE;
1291
1581
  _scenePass.clearStencil = 0;
1292
1582
  } else {
1293
- _scenePass.clearDepth = depthTex ? null : 1;
1583
+ _scenePass.clearDepth = depthTex ? null : DEPTH_CLEAR_VALUE;
1294
1584
  _scenePass.clearStencil = depthTex ? null : 0;
1295
1585
  }
1296
1586
  ctx.materialFlags = sceneColorMaterialFlags;
@@ -1308,11 +1598,13 @@ function blitToCurrentColorAttachment(ctx, source) {
1308
1598
  }
1309
1599
  /** @internal */ function renderOpaqueScenePass(frame, sceneColorTex, sceneColorCopyTex, rgCtx, sceneColorFramebufferHandle) {
1310
1600
  const { ctx, renderQueue } = frame;
1311
- // Use RenderGraph-allocated scene color texture
1312
1601
  const depthTex = frame.depthFramebuffer?.getDepthAttachment();
1313
- 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);
1314
- if (ctx.SSR) {
1315
- ctx.materialFlags |= MaterialVaryingFlags.SSR_STORE_ROUGHNESS;
1602
+ ctx.materialFlags &= ~(MaterialVaryingFlags.SCENE_STORE_ROUGHNESS | MaterialVaryingFlags.SSS_STORE_PROFILE | MaterialVaryingFlags.SSS_STORE_DIFFUSE | MaterialVaryingFlags.SCENE_STORE_NORMAL | MaterialVaryingFlags.SSS_STORE_TRANSMISSION | MaterialVaryingFlags.SKIN_SSS_STORE);
1603
+ if (ctx.SceneRoughnessTexture) {
1604
+ ctx.materialFlags |= MaterialVaryingFlags.SCENE_STORE_ROUGHNESS;
1605
+ }
1606
+ if (ctx.SceneNormalTexture) {
1607
+ ctx.materialFlags |= MaterialVaryingFlags.SCENE_STORE_NORMAL;
1316
1608
  }
1317
1609
  if (ctx.SSS) {
1318
1610
  if (ctx.SSSDiffuseTexture) {
@@ -1325,11 +1617,7 @@ function blitToCurrentColorAttachment(ctx, source) {
1325
1617
  if (ctx.SkinSSSTexture) {
1326
1618
  ctx.materialFlags |= MaterialVaryingFlags.SKIN_SSS_STORE;
1327
1619
  }
1328
- // The graph scene color framebuffer takes priority: it is absent only when
1329
- // the graph was built in final-framebuffer-as-intermediate mode (external
1330
- // depth shared with the final framebuffer AND no opaque-layer effects).
1331
- // Checking the shared depth first would wrongly route the MRT/opaque-effect
1332
- // case into the single-color final framebuffer.
1620
+ // Prefer the graph framebuffer; final-target mode has no graph framebuffer.
1333
1621
  if (sceneColorFramebufferHandle && !hasSurfaceMRT(ctx)) {
1334
1622
  ctx.intermediateFramebuffer = rgCtx.getFramebuffer(sceneColorFramebufferHandle);
1335
1623
  } else if (!sceneColorFramebufferHandle && depthTex === ctx.finalFramebuffer?.getDepthAttachment()) {
@@ -1342,32 +1630,23 @@ function blitToCurrentColorAttachment(ctx, source) {
1342
1630
  depthAttachment: depthTex
1343
1631
  });
1344
1632
  }
1345
- // The scene target is bound explicitly and the device state is restored at
1346
- // the end of this pass: graph passes never communicate through leftover
1347
- // device state. TransparentPass re-binds the same target (or the opaque
1348
- // chain output) explicitly.
1633
+ // Graph passes do not communicate through leftover device state.
1349
1634
  ctx.device.pushDeviceStates();
1350
1635
  try {
1351
- // setFramebuffer() no-ops when the target is unchanged, so reset the
1352
- // viewport/scissor explicitly to cover that case.
1636
+ // setFramebuffer may no-op, so reset viewport and scissor explicitly.
1353
1637
  ctx.device.setFramebuffer(ctx.intermediateFramebuffer);
1354
1638
  ctx.device.setViewport(null);
1355
1639
  ctx.device.setScissor(null);
1356
1640
  _scenePass.transmission = false;
1357
- _scenePass.clearDepth = depthTex ? null : 1;
1641
+ _scenePass.renderSky = false;
1642
+ _scenePass.clearDepth = depthTex ? null : DEPTH_CLEAR_VALUE;
1358
1643
  _scenePass.clearStencil = depthTex ? null : 0;
1359
1644
  if (renderQueue.needSceneColor() && sceneColorCopyTex) {
1360
- // Background copy was produced by the SceneColorGrab pass; seed the main
1361
- // color attachment with it and render only transmission/transparent on top.
1645
+ // Seed the main target with the refraction background.
1362
1646
  ctx.sceneColorTexture = sceneColorCopyTex;
1363
1647
  blitToCurrentColorAttachment(ctx, ctx.sceneColorTexture);
1364
1648
  if (hasSurfaceMRT(ctx)) {
1365
- // The background copy carries no surface MRT attachments, so opaque
1366
- // geometry exists only there without roughness/normal (and SSS
1367
- // lighting) data. Re-render the opaque lists into the MRT scene
1368
- // target: early-z against the prepass depth keeps this cheap, and the
1369
- // color output matches the blitted copy on opaque pixels while the
1370
- // MRT attachments receive the surface data SSR/SSS require.
1649
+ // Re-render opaque lists to populate MRT data missing from the copy.
1371
1650
  _scenePass.clearColor = null;
1372
1651
  _scenePass.clearDepth = null;
1373
1652
  _scenePass.clearStencil = null;
@@ -1384,28 +1663,49 @@ function blitToCurrentColorAttachment(ctx, source) {
1384
1663
  _scenePass.renderTransparent = false;
1385
1664
  _scenePass.render(ctx, null, null, renderQueue);
1386
1665
  } finally{
1387
- // Restore the shared _scenePass flags so no state leaks to the next pass:
1388
- // the transparent pass sets transmission/renderOpaque/renderTransparent
1389
- // itself rather than inheriting whatever this pass happened to leave.
1666
+ // Restore shared pass flags before the next graph pass.
1390
1667
  _scenePass.renderTransparent = true;
1668
+ _scenePass.renderSky = true;
1391
1669
  _scenePass.transmission = false;
1392
1670
  ctx.device.popDeviceStates();
1393
1671
  }
1394
1672
  }
1395
- /**
1396
- * Renders the transmission/transparent geometry lists (including OIT) on top
1397
- * of the opaque result. The target framebuffer is always bound explicitly:
1398
- * the opaque chain output when opaque-layer effects ran, otherwise the same
1399
- * scene target the light pass rendered into. OIT implementations composite
1400
- * into the currently bound framebuffer, which this pass guarantees.
1401
- * @internal
1402
- */ function renderTransparentScenePass(frame, rgCtx, opaqueChainOutput, sceneColorFramebufferHandle) {
1673
+ /** Render sky and fog after opaque geometry. @internal */ function renderSkyScenePass(frame, rgCtx, sceneColorFramebufferHandle, sceneColorNoFog) {
1674
+ const { ctx } = frame;
1675
+ const framebuffer = sceneColorFramebufferHandle ? rgCtx.getFramebuffer(sceneColorFramebufferHandle) : ctx.finalFramebuffer;
1676
+ const device = ctx.device;
1677
+ device.pushDeviceStates();
1678
+ try {
1679
+ device.setFramebuffer(framebuffer);
1680
+ device.setViewport(null);
1681
+ device.setScissor(null);
1682
+ ctx.scene.env.sky.renderSky(ctx);
1683
+ if (ctx.scene.env.sky.fogPresents) {
1684
+ // Snapshot between sky and fog. Sky belongs in the copy - a ray that hits
1685
+ // the sky should read the sky - while fog does not, because the fog a
1686
+ // screen-space pass would sample lies along the camera ray rather than the
1687
+ // path it integrates.
1688
+ if (sceneColorNoFog) {
1689
+ const source = framebuffer?.getColorAttachment(0) ?? null;
1690
+ if (source) {
1691
+ new CopyBlitter().blit(source, sceneColorNoFog, fetchSampler('clamp_nearest_nomip'));
1692
+ device.setFramebuffer(framebuffer);
1693
+ device.setViewport(null);
1694
+ device.setScissor(null);
1695
+ }
1696
+ }
1697
+ ctx.scene.env.sky.renderFog(ctx.camera);
1698
+ }
1699
+ } finally{
1700
+ device.popDeviceStates();
1701
+ }
1702
+ }
1703
+ /** Render transmission, transparency, and OIT over the opaque result. @internal */ function renderTransparentScenePass(frame, rgCtx, opaqueChainOutput, sceneColorFramebufferHandle) {
1403
1704
  const { ctx, renderQueue } = frame;
1404
1705
  const device = ctx.device;
1405
1706
  let framebuffer;
1406
1707
  if (opaqueChainOutput) {
1407
- // Opaque-layer effects redirected the scene color into their chain output;
1408
- // transparent geometry renders on top of it with the scene depth attached.
1708
+ // Opaque effects redirect transparent rendering to their chain output.
1409
1709
  const chainTex = rgCtx.getTexture(opaqueChainOutput);
1410
1710
  const depthTex = frame.depthFramebuffer?.getDepthAttachment();
1411
1711
  framebuffer = rgCtx.createFramebuffer({
@@ -1414,39 +1714,34 @@ function blitToCurrentColorAttachment(ctx, source) {
1414
1714
  colorAttachments: chainTex,
1415
1715
  depthAttachment: depthTex
1416
1716
  });
1417
- // The chain output is single-color: surface MRT stores no longer apply.
1717
+ // The chain output has no surface MRT attachments.
1418
1718
  ctx.materialFlags &= ~SURFACE_MRT_FLAGS;
1419
1719
  } else if (sceneColorFramebufferHandle) {
1420
- // No opaque-layer effect ran (thus no surface MRT either): continue in the
1421
- // graph scene color framebuffer the light pass rendered into.
1720
+ // Continue in the light pass target when no opaque effect ran.
1422
1721
  framebuffer = rgCtx.getFramebuffer(sceneColorFramebufferHandle);
1423
1722
  } else {
1424
- // Final framebuffer used as scene intermediate.
1723
+ // Scene color already resides in the final framebuffer.
1425
1724
  framebuffer = ctx.finalFramebuffer;
1426
1725
  }
1427
1726
  device.pushDeviceStates();
1428
1727
  try {
1429
- // setFramebuffer() no-ops when the target is unchanged, so reset the
1430
- // viewport/scissor explicitly to cover that case.
1728
+ // setFramebuffer may no-op, so reset viewport and scissor explicitly.
1431
1729
  device.setFramebuffer(framebuffer);
1432
1730
  device.setViewport(null);
1433
1731
  device.setScissor(null);
1434
- // Derive transmission mode from the same single source of truth the opaque
1435
- // pass used (renderQueue.needSceneColor()) instead of inheriting leftover
1436
- // _scenePass state. When scene color is needed the opaque pass seeded the
1437
- // refraction background and drew the transmission-opaque list, so here we
1438
- // draw transmission_trans + transparent; otherwise just the transparent
1439
- // list. Never clear: the opaque result is already in the target.
1732
+ // Derive transmission mode instead of inheriting shared pass state.
1440
1733
  _scenePass.transmission = renderQueue.needSceneColor();
1441
1734
  _scenePass.clearColor = null;
1442
1735
  _scenePass.clearDepth = null;
1443
1736
  _scenePass.clearStencil = null;
1444
1737
  _scenePass.renderOpaque = false;
1445
1738
  _scenePass.renderTransparent = true;
1739
+ _scenePass.renderSky = false;
1446
1740
  try {
1447
1741
  _scenePass.render(ctx, null, null, renderQueue);
1448
1742
  } finally{
1449
1743
  _scenePass.renderOpaque = true;
1744
+ _scenePass.renderSky = true;
1450
1745
  }
1451
1746
  } finally{
1452
1747
  device.popDeviceStates();
@@ -1455,38 +1750,7 @@ function blitToCurrentColorAttachment(ctx, source) {
1455
1750
  /** @internal */ function renderTransmissionDepthPass(frame, rgCtx) {
1456
1751
  renderSceneDepth(frame, frame.depthFramebuffer, rgCtx);
1457
1752
  }
1458
- /**
1459
- * Frame-tail housekeeping shared by the Present and FrameCleanup passes.
1460
- *
1461
- * The end-layer post effect chain and the final blit are graph passes now
1462
- * (see buildForwardPlusGraphInternal step 9/10); this only releases per-frame
1463
- * state. The compositor's scene ping-pong is flushed back to the intermediate
1464
- * framebuffer by compositor.end() at the tail of the light pass.
1465
- *
1466
- * @internal
1467
- */ function finishFrame(frame) {
1468
- const { ctx } = frame;
1469
- disposeRenderQueue(frame);
1470
- ctx.materialFlags &= ~MaterialVaryingFlags.SSR_STORE_ROUGHNESS;
1471
- ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_PROFILE;
1472
- ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_DIFFUSE;
1473
- ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_NORMAL;
1474
- ctx.materialFlags &= ~MaterialVaryingFlags.SSS_STORE_TRANSMISSION;
1475
- ctx.materialFlags &= ~MaterialVaryingFlags.SKIN_SSS_STORE;
1476
- releaseIntermediateFramebuffer(frame);
1477
- releaseDepthFramebuffer(frame);
1478
- releaseClusteredLight(frame);
1479
- restoreSunLight(frame);
1480
- }
1481
- // ─── Convenience: Execute Full Pipeline ─────────────────────────────
1482
- /**
1483
- * Build, compile, and execute the forward+ pipeline as a render graph.
1484
- *
1485
- * This is the drop-in replacement for `SceneRenderer._renderScene`.
1486
- *
1487
- * @param ctx - The draw context for this frame.
1488
- * @public
1489
- */ function executeForwardPlusGraph(ctx) {
1753
+ /** Build and execute the Forward+ graph for one frame. @public */ function executeForwardPlusGraph(ctx) {
1490
1754
  const device = ctx.device;
1491
1755
  const graph = new RenderGraph();
1492
1756
  let renderQueue = null;
@@ -1494,34 +1758,55 @@ function blitToCurrentColorAttachment(ctx, source) {
1494
1758
  let executor = null;
1495
1759
  let historyManager = null;
1496
1760
  let historyFrameStarted = false;
1761
+ let motionVectorFramePrepared = false;
1762
+ let motionVectorFrameCommitted = false;
1497
1763
  try {
1498
- // Cull scene first (needed to derive options)
1499
1764
  renderQueue = _scenePass.cullScene(ctx, ctx.camera);
1500
1765
  const options = deriveForwardPlusOptions(ctx.scene, ctx.camera, device.type, renderQueue);
1766
+ options.ssgi &&= supportsSSGIRenderTargets(ctx);
1501
1767
  ctx.SSS = options.sss;
1502
- // Ensure the camera has a history resource manager for temporal effects (TAA, motion blur)
1503
1768
  historyManager = ctx.camera.getHistoryResourceManager();
1504
1769
  if (!historyManager) {
1505
1770
  historyManager = new HistoryResourceManager(_devicePoolAllocator);
1506
1771
  ctx.camera.setHistoryResourceManager(historyManager);
1507
1772
  }
1773
+ if (!options.ssgi) {
1774
+ historyManager.invalidate(RGHistoryResources.SSGI_SCENE_COLOR);
1775
+ historyManager.invalidate(RGHistoryResources.SSGI_IRRADIANCE);
1776
+ historyManager.invalidate(RGHistoryResources.SSGI_SURFACE);
1777
+ historyManager.invalidate(RGHistoryResources.SSGI_MOMENTS);
1778
+ }
1508
1779
  historyManager.beginFrame();
1509
1780
  historyFrameStarted = true;
1510
1781
  const buildResult = buildForwardPlusGraphInternal(graph, ctx, renderQueue, options);
1511
1782
  frame = buildResult.frame;
1783
+ if (options.motionVectors) {
1784
+ ctx.camera.prepareMotionVectorFrame(ctx.camera.TAA, ctx.renderWidth, ctx.renderHeight);
1785
+ motionVectorFramePrepared = true;
1786
+ } else {
1787
+ ctx.camera.clearMotionVectorFrame();
1788
+ }
1512
1789
  const compiled = graph.compile([
1513
1790
  buildResult.backbuffer
1514
1791
  ]);
1515
- // Use RenderGraphExecutor for automatic resource management
1516
- executor = new RenderGraphExecutor(_devicePoolAllocator, ctx.renderWidth, ctx.renderHeight);
1517
- // Register imported backbuffer (if using finalFramebuffer)
1792
+ executor = new RenderGraphExecutor(_devicePoolAllocator, ctx.renderWidth, ctx.renderHeight, {
1793
+ textureAffinityCache: getTextureAffinityCache(ctx.camera, device)
1794
+ });
1518
1795
  if (ctx.finalFramebuffer) {
1519
1796
  const backbufferTex = ctx.finalFramebuffer.getColorAttachments()[0];
1520
1797
  executor.setImportedTexture(buildResult.backbuffer, backbufferTex);
1521
1798
  }
1799
+ if (buildResult.externalDepthImport) {
1800
+ executor.setImportedTexture(buildResult.externalDepthImport.handle, buildResult.externalDepthImport.texture);
1801
+ }
1522
1802
  historyManager.bindImportedTextures(executor);
1523
1803
  executor.execute(compiled);
1524
1804
  historyManager.commitFrame();
1805
+ historyFrameStarted = false;
1806
+ if (motionVectorFramePrepared) {
1807
+ ctx.camera.commitMotionVectorFrame();
1808
+ motionVectorFrameCommitted = true;
1809
+ }
1525
1810
  } finally{
1526
1811
  if (historyFrameStarted) {
1527
1812
  historyManager?.discardFrame();
@@ -1532,8 +1817,11 @@ function blitToCurrentColorAttachment(ctx, source) {
1532
1817
  renderQueue?.dispose();
1533
1818
  }
1534
1819
  executor?.reset();
1820
+ if (motionVectorFramePrepared && !motionVectorFrameCommitted) {
1821
+ ctx.camera.clearMotionVectorFrame();
1822
+ }
1535
1823
  }
1536
1824
  }
1537
1825
 
1538
- export { buildForwardPlusGraph, deriveForwardPlusOptions, executeForwardPlusGraph };
1826
+ export { ForwardPlusModules, buildForwardPlusGraph, createForwardPlusPipeline, deriveForwardPlusOptions, executeForwardPlusGraph, getDefaultForwardPlusPipeline, renderOpaqueScenePass, renderSkyScenePass, renderTransparentScenePass };
1539
1827
  //# sourceMappingURL=forward_plus_builder.js.map