@zephyr3d/scene 0.9.22 → 0.9.24

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 (234) 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 +9113 -8740
  30. package/dist/index.js +10 -2
  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 +512 -334
  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 +35 -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 +988 -436
  83. package/dist/posteffect/ssgi.js.map +1 -1
  84. package/dist/posteffect/ssr.js +83 -45
  85. package/dist/posteffect/ssr.js.map +1 -1
  86. package/dist/posteffect/sss.js +18 -15
  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 +22 -16
  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 +1 -1
  121. package/dist/render/rendergraph/frame_graph_context.js.map +1 -1
  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 +7 -6
  143. package/dist/render/shadow_mask_pass.js.map +1 -1
  144. package/dist/render/sky.js +257 -95
  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 +149 -40
  177. package/dist/shaders/shadow.js.map +1 -1
  178. package/dist/shaders/ssgi_reprojection.js +118 -0
  179. package/dist/shaders/ssgi_reprojection.js.map +1 -0
  180. package/dist/shaders/ssr.js +322 -154
  181. package/dist/shaders/ssr.js.map +1 -1
  182. package/dist/shadow/esm.js +10 -7
  183. package/dist/shadow/esm.js.map +1 -1
  184. package/dist/shadow/pcf_opt.js +12 -12
  185. package/dist/shadow/pcf_opt.js.map +1 -1
  186. package/dist/shadow/pcf_pd.js +12 -12
  187. package/dist/shadow/pcf_pd.js.map +1 -1
  188. package/dist/shadow/pcss.js +16 -11
  189. package/dist/shadow/pcss.js.map +1 -1
  190. package/dist/shadow/shadow_impl.js +29 -3
  191. package/dist/shadow/shadow_impl.js.map +1 -1
  192. package/dist/shadow/shadowmapper.js +5 -2
  193. package/dist/shadow/shadowmapper.js.map +1 -1
  194. package/dist/shadow/ssm.js +13 -13
  195. package/dist/shadow/ssm.js.map +1 -1
  196. package/dist/shadow/vsm.js +7 -7
  197. package/dist/shadow/vsm.js.map +1 -1
  198. package/dist/utility/physical.js +74 -0
  199. package/dist/utility/physical.js.map +1 -0
  200. package/dist/utility/rendermipmap.js +2 -2
  201. package/dist/utility/rendermipmap.js.map +1 -1
  202. package/dist/utility/serialization/manager.js +3 -1
  203. package/dist/utility/serialization/manager.js.map +1 -1
  204. package/dist/utility/serialization/scene/animation.js +3 -1
  205. package/dist/utility/serialization/scene/animation.js.map +1 -1
  206. package/dist/utility/serialization/scene/batch.js +3 -1
  207. package/dist/utility/serialization/scene/batch.js.map +1 -1
  208. package/dist/utility/serialization/scene/camera.js +598 -2
  209. package/dist/utility/serialization/scene/camera.js.map +1 -1
  210. package/dist/utility/serialization/scene/light.js +180 -9
  211. package/dist/utility/serialization/scene/light.js.map +1 -1
  212. package/dist/utility/serialization/scene/material.js +43 -3
  213. package/dist/utility/serialization/scene/material.js.map +1 -1
  214. package/dist/utility/serialization/scene/mesh.js +3 -1
  215. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  216. package/dist/utility/serialization/scene/node.js +3 -1
  217. package/dist/utility/serialization/scene/node.js.map +1 -1
  218. package/dist/utility/serialization/scene/particle.js +3 -1
  219. package/dist/utility/serialization/scene/particle.js.map +1 -1
  220. package/dist/utility/serialization/scene/primitive.js +2 -0
  221. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  222. package/dist/utility/serialization/scene/scene.js +70 -0
  223. package/dist/utility/serialization/scene/scene.js.map +1 -1
  224. package/dist/utility/serialization/scene/sprite.js +3 -1
  225. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  226. package/dist/utility/serialization/scene/terrain.js +3 -1
  227. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  228. package/dist/utility/serialization/scene/text.js +3 -0
  229. package/dist/utility/serialization/scene/text.js.map +1 -1
  230. package/dist/utility/serialization/scene/water.js +3 -1
  231. package/dist/utility/serialization/scene/water.js.map +1 -1
  232. package/dist/values.js +59 -65
  233. package/dist/values.js.map +1 -1
  234. package/package.json +3 -3
@@ -1,10 +1,7 @@
1
- import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
2
- import { linearToGamma } from '../shaders/misc.js';
1
+ import { Vector2, Matrix4x4, Vector4, DEPTH_FARTHEST } from '@zephyr3d/base';
2
+ import { FrameResources } from '../render/rendergraph/blackboard.js';
3
+ import { RGHistoryResources } from '../render/rendergraph/history_resources.js';
3
4
  import { screenSpaceRayTracing_HiZ, screenSpaceRayTracing_Linear2D } from '../shaders/ssr.js';
4
- import { SSGI_rand2, SSGI_cosineSampleHemisphere } from '../shaders/ssgi.js';
5
- import { Vector2, Matrix4x4, Vector4 } from '@zephyr3d/base';
6
- import { copyTexture, fetchSampler } from '../utility/misc.js';
7
- import { BilateralBlurBlitter } from '../blitter/bilateralblur.js';
8
5
  import { ShaderHelper } from '../material/shader/helper.js';
9
6
  import '../material/lambert.js';
10
7
  import '../material/blinn.js';
@@ -24,92 +21,203 @@ import '../material/pbrblueprint.js';
24
21
  import '../material/sprite.js';
25
22
  import '../material/sprite_std.js';
26
23
  import '../utility/blueprint/material/texture.js';
27
- import { RGHistoryResources } from '../render/rendergraph/history_resources.js';
28
- import { FrameResources } from '../render/rendergraph/blackboard.js';
24
+ import { linearToGamma } from '../shaders/misc.js';
25
+ import { fetchSampler } from '../utility/misc.js';
26
+ import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
29
27
 
30
- /**
31
- * SSGI post effect
32
- *
33
- * @remarks
34
- * Screen space global illumination: gathers one-bounce indirect diffuse
35
- * lighting by cosine-weighted hemisphere ray marching against the scene
36
- * depth buffer (Hi-Z accelerated when available), then denoises the result
37
- * with a bilateral blur and optional temporal accumulation before adding
38
- * it to the scene color. Internal used in light pass.
39
- *
40
- * @internal
28
+ const IRRADIANCE_FORMAT = 'rgba16f';
29
+ const SURFACE_FORMAT = 'rgba16f';
30
+ const MOMENTS_FORMAT = 'rgba16f';
31
+ // AO is scalar; r16f is available wherever SSGI runs.
32
+ const AO_FORMAT = 'r16f';
33
+ const HALF_FLOAT_MAX = 65504;
34
+ // Keep squared luminance below the rgba16f range.
35
+ const MOMENT_LUMINANCE_MAX = 255;
36
+ /**
37
+ * Screen-space diffuse global illumination.
38
+ *
39
+ * @public
41
40
  */ class SSGI extends AbstractPostEffect {
42
41
  static _tracePrograms = {};
43
- static _combineProgram;
44
- static _temporalProgram;
45
- static _blurBlitterH = null;
46
- static _blurBlitterV = null;
47
- _traceBindGroups;
48
- _combineBindGroup;
49
- _temporalBindGroup;
50
- /**
51
- * Creates an instance of SSGI post effect
52
- */ constructor(){
42
+ static _temporalPrograms = {};
43
+ static _atrousProgram = null;
44
+ static _surfaceProgram = null;
45
+ static _upsampleProgram = null;
46
+ static _compositePrograms = {};
47
+ _traceBindGroups = {};
48
+ _temporalBindGroups = {};
49
+ _atrousBindGroups = {};
50
+ _surfaceBindGroups = {};
51
+ _upsampleBindGroups = {};
52
+ _compositeBindGroups = {};
53
+ constructor(){
53
54
  super();
54
55
  this._layer = PostEffectLayer.opaque;
55
- this._traceBindGroups = {};
56
- this._combineBindGroup = null;
57
- this._temporalBindGroup = null;
58
56
  }
59
- /** {@inheritDoc AbstractPostEffect.requireLinearDepthTexture} */ requireLinearDepthTexture() {
60
- return true;
61
- }
62
- /** {@inheritDoc AbstractPostEffect.requireDepthAttachment} */ requireDepthAttachment() {
63
- return true;
57
+ /** {@inheritDoc AbstractPostEffect.requireLinearDepthTexture} */ requireLinearDepthTexture(ctx) {
58
+ return !!ctx.SSGI;
64
59
  }
65
60
  /** {@inheritDoc AbstractPostEffect.requireMotionVectorTexture} */ requireMotionVectorTexture(ctx) {
66
- return !!ctx.camera.ssgiTemporal;
61
+ return !!ctx.SSGI && ctx.device.type === 'webgpu';
62
+ }
63
+ /** {@inheritDoc AbstractPostEffect.requireHiZTexture} */ requireHiZTexture(ctx) {
64
+ return !!ctx.SSGI && ctx.device.type === 'webgpu';
67
65
  }
68
- /**
69
- * Declares SSGI's internal steps (trace, optional bilateral blur, optional
70
- * temporal resolve, combine) as individual render graph passes.
71
- */ setup(s) {
72
- const { graph, ctx, history } = s;
73
- // SSGI needs the surface normal MRT and dynamic loops; neither is
74
- // available on WebGL1. Pass the input through untouched in that case.
75
- if (ctx.device.type === 'webgl' || !s.blackboard.get(FrameResources.SSRNormal)) {
66
+ /** {@inheritDoc AbstractPostEffect.requireSceneNormalTexture} */ requireSceneNormalTexture(ctx) {
67
+ return !!ctx.SSGI;
68
+ }
69
+ /** Build trace, temporal, denoise, upsample, history and pass-through stages. */ setup(s) {
70
+ const { graph, ctx, history, blackboard } = s;
71
+ if (!ctx.SSGI || !history) {
76
72
  return s.input;
77
73
  }
78
- const camera = ctx.camera;
79
- const halfRes = !!camera.ssgiHalfResolution;
80
- const traceWidth = Math.max(1, halfRes ? s.width >> 1 : s.width);
81
- const traceHeight = Math.max(1, halfRes ? s.height >> 1 : s.height);
82
- const linearDepthHandle = s.blackboard.get(FrameResources.LinearDepth);
83
- const texDesc = {
84
- format: 'rgba16f',
74
+ const linearDepthHandle = blackboard.get(FrameResources.LinearDepth);
75
+ const normalHandle = blackboard.get(FrameResources.SceneNormal);
76
+ const motionHandle = blackboard.get(FrameResources.MotionVector);
77
+ const hiZHandle = blackboard.get(FrameResources.HiZ);
78
+ // Traced hits read this as outgoing surface radiance, so it must exclude the
79
+ // fog the camera ray accumulated: that fog belongs to a different path than
80
+ // the one being integrated, and feeding it back would brighten every bounce.
81
+ // Absent when the scene has no fog, in which case SceneColor is already fog
82
+ // free. Both the sampled color and the committed history use it, otherwise
83
+ // the current and previous frames would disagree and flicker.
84
+ const noFogHandle = blackboard.get(FrameResources.SceneColorNoFog);
85
+ const sampleColorHandle = noFogHandle ?? s.input;
86
+ if (!linearDepthHandle || !normalHandle) {
87
+ return s.input;
88
+ }
89
+ const settings = ctx.camera.ssgiResolvedSettings;
90
+ const traceWidth = settings.halfRes ? Math.max(1, Math.ceil(s.width * 0.5)) : s.width;
91
+ const traceHeight = settings.halfRes ? Math.max(1, Math.ceil(s.height * 0.5)) : s.height;
92
+ const traceSize = {
93
+ width: traceWidth,
94
+ height: traceHeight
95
+ };
96
+ const fullSize = {
97
+ width: s.width,
98
+ height: s.height
99
+ };
100
+ const traceDesc = {
101
+ format: IRRADIANCE_FORMAT,
85
102
  sizeMode: 'absolute',
86
103
  width: traceWidth,
87
104
  height: traceHeight
88
105
  };
89
- const readCommon = (builder)=>{
90
- if (linearDepthHandle) {
91
- builder.read(linearDepthHandle);
106
+ const fullIrradianceDesc = {
107
+ format: IRRADIANCE_FORMAT,
108
+ sizeMode: 'absolute',
109
+ width: s.width,
110
+ height: s.height
111
+ };
112
+ const surfaceDesc = {
113
+ format: SURFACE_FORMAT,
114
+ sizeMode: 'absolute',
115
+ width: s.width,
116
+ height: s.height
117
+ };
118
+ const momentsDesc = {
119
+ format: MOMENTS_FORMAT,
120
+ sizeMode: 'absolute',
121
+ width: traceWidth,
122
+ height: traceHeight
123
+ };
124
+ const aoTraceDesc = {
125
+ format: AO_FORMAT,
126
+ sizeMode: 'absolute',
127
+ width: traceWidth,
128
+ height: traceHeight
129
+ };
130
+ const fullAODesc = {
131
+ format: AO_FORMAT,
132
+ sizeMode: 'absolute',
133
+ width: s.width,
134
+ height: s.height
135
+ };
136
+ const findHistoryRead = (name)=>s.historyReads.find((binding)=>binding.name === name)?.handle ?? null;
137
+ const previousIrradianceHandle = findHistoryRead(RGHistoryResources.SSGI_IRRADIANCE);
138
+ const previousSurfaceHandle = findHistoryRead(RGHistoryResources.SSGI_SURFACE);
139
+ const previousMomentsHandle = history.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_MOMENTS, momentsDesc, traceSize);
140
+ // Only this effect's own temporal pass reads the AO history, so it is
141
+ // imported directly instead of being routed through the light pass history
142
+ // read bindings the way irradiance and surface are.
143
+ const previousAOHandle = history.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_AO, aoTraceDesc, traceSize);
144
+ const previousSceneColorHandle = ctx.device.type === 'webgpu' ? history.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_SCENE_COLOR, {
145
+ format: s.colorFormat,
146
+ sizeMode: 'absolute',
147
+ width: s.width,
148
+ height: s.height
149
+ }, fullSize) : null;
150
+ const canSampleSceneHistory = !!(previousSceneColorHandle && previousSurfaceHandle && motionHandle);
151
+ const readFrameInputs = (builder)=>{
152
+ builder.read(linearDepthHandle);
153
+ builder.read(normalHandle);
154
+ if (motionHandle) {
155
+ builder.read(motionHandle);
156
+ }
157
+ if (hiZHandle) {
158
+ builder.read(hiZHandle);
92
159
  }
93
- // Normal MRT, HiZ and the scene color are reached through DrawContext
94
- // fields; the dependency list carries their handles.
95
160
  for (const dep of s.dependencies){
96
161
  builder.read(dep);
97
162
  }
98
163
  };
99
- const getLinearDepth = (rg)=>linearDepthHandle ? rg.getTexture(linearDepthHandle) : ctx.linearDepthTexture;
100
- // 1. Trace cosine-weighted hemisphere rays against the depth buffer and
101
- // gather the radiance at the hit points.
102
- const traceHandle = graph.addPass('SSGI:Trace', (builder)=>{
103
- builder.read(s.input);
104
- readCommon(builder);
164
+ // Trace one or two cosine-weighted diffuse rays. A hit prefers the previous
165
+ // frame through previousHitUV = hitUV - motion(hitUV) for multi-bounce
166
+ // feedback and falls back to the current frame when that reprojection fails.
167
+ const traceResult = graph.addPass('SSGI:Trace', (builder)=>{
168
+ builder.read(sampleColorHandle);
169
+ readFrameInputs(builder);
170
+ if (canSampleSceneHistory) {
171
+ builder.read(previousSceneColorHandle);
172
+ builder.read(previousSurfaceHandle);
173
+ }
105
174
  const out = builder.createTexture({
106
- ...texDesc,
107
- label: 'SSGI:trace'
175
+ ...traceDesc,
176
+ label: 'SSGI:rawIrradiance'
177
+ });
178
+ const ao = builder.createTexture({
179
+ ...aoTraceDesc,
180
+ label: 'SSGI:rawAO'
108
181
  });
109
182
  const fb = builder.createFramebuffer({
110
183
  label: 'SSGI:traceFB',
111
184
  width: traceWidth,
112
185
  height: traceHeight,
186
+ colorAttachments: [
187
+ out,
188
+ ao
189
+ ],
190
+ depthAttachment: null,
191
+ ignoreDepthStencil: true
192
+ });
193
+ builder.setExecute((rg)=>{
194
+ const device = ctx.device;
195
+ device.pushDeviceStates();
196
+ try {
197
+ device.setFramebuffer(rg.getFramebuffer(fb));
198
+ this.trace(ctx, rg.getTexture(sampleColorHandle), canSampleSceneHistory ? rg.getTexture(previousSceneColorHandle) : null, rg.getTexture(linearDepthHandle), rg.getTexture(normalHandle), canSampleSceneHistory ? rg.getTexture(motionHandle) : null, canSampleSceneHistory ? rg.getTexture(previousSurfaceHandle) : null, hiZHandle ? rg.getTexture(hiZHandle) : null, traceWidth, traceHeight);
199
+ } finally{
200
+ device.popDeviceStates();
201
+ }
202
+ });
203
+ return {
204
+ irradiance: out,
205
+ ao
206
+ };
207
+ });
208
+ // Store the current full-resolution surface independently from irradiance;
209
+ // no albedo MRT is needed because the next lighting pass applies the BRDF.
210
+ const surfaceHandle = graph.addPass('SSGI:Surface', (builder)=>{
211
+ builder.read(linearDepthHandle);
212
+ builder.read(normalHandle);
213
+ const out = builder.createTexture({
214
+ ...surfaceDesc,
215
+ label: 'SSGI:surface'
216
+ });
217
+ const fb = builder.createFramebuffer({
218
+ label: 'SSGI:surfaceFB',
219
+ width: s.width,
220
+ height: s.height,
113
221
  colorAttachments: out,
114
222
  depthAttachment: null,
115
223
  ignoreDepthStencil: true
@@ -119,52 +227,97 @@ import { FrameResources } from '../render/rendergraph/blackboard.js';
119
227
  device.pushDeviceStates();
120
228
  try {
121
229
  device.setFramebuffer(rg.getFramebuffer(fb));
122
- this.trace(ctx, rg.getTexture(s.input), getLinearDepth(rg));
230
+ this.writeSurface(ctx, rg.getTexture(linearDepthHandle), rg.getTexture(normalHandle));
123
231
  } finally{
124
232
  device.popDeviceStates();
125
233
  }
126
234
  });
127
235
  return out;
128
236
  });
129
- // 2. Temporal accumulation BEFORE the spatial blur: accumulating the raw
130
- // per-pixel estimate raises the effective sample count over time, and the
131
- // blur then only has to clean the residual noise. (Blurring first would
132
- // turn per-frame noise into large low-frequency blobs that flicker.)
133
- // The history stores the pre-blur accumulated signal.
134
- const wantTemporal = !!camera.ssgiTemporal;
135
- const motionVectorHandle = wantTemporal ? s.blackboard.get(FrameResources.MotionVector) : null;
136
- const historySize = {
137
- width: traceWidth,
138
- height: traceHeight
139
- };
140
- const prevRadianceHandle = wantTemporal && history && motionVectorHandle ? history.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_RADIANCE, texDesc, historySize) : null;
141
- const prevMotionVectorHandle = wantTemporal && history && motionVectorHandle ? history.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_MOTION_VECTOR, {
142
- format: 'rgba16f',
143
- sizeMode: 'absolute',
144
- width: s.width,
145
- height: s.height
146
- }, {
147
- width: s.width,
148
- height: s.height
149
- }) : null;
150
- let radianceHandle = traceHandle;
151
- const canTemporal = !!(motionVectorHandle && prevRadianceHandle && prevMotionVectorHandle);
152
- if (canTemporal) {
153
- radianceHandle = graph.addPass('SSGI:Temporal', (builder)=>{
154
- builder.read(traceHandle);
155
- builder.read(prevRadianceHandle);
156
- builder.read(prevMotionVectorHandle);
157
- builder.read(motionVectorHandle);
158
- readCommon(builder);
237
+ const canTemporal = !!(ctx.device.type === 'webgpu' && ctx.camera.ssgiTemporal && motionHandle && previousIrradianceHandle && previousSurfaceHandle && previousMomentsHandle && previousAOHandle);
238
+ // Temporal pass always creates moments. On the first frame it simply
239
+ // initializes them; with valid history it rejects by depth and normal,
240
+ // neighborhood-clamps radiance, and accumulates first/second moments.
241
+ const temporalResult = graph.addPass('SSGI:Temporal', (builder)=>{
242
+ builder.read(traceResult.irradiance);
243
+ builder.read(traceResult.ao);
244
+ readFrameInputs(builder);
245
+ if (canTemporal) {
246
+ builder.read(previousIrradianceHandle);
247
+ builder.read(previousSurfaceHandle);
248
+ builder.read(previousMomentsHandle);
249
+ builder.read(previousAOHandle);
250
+ }
251
+ const irradiance = builder.createTexture({
252
+ ...traceDesc,
253
+ label: 'SSGI:temporalIrradiance'
254
+ });
255
+ const moments = builder.createTexture({
256
+ ...momentsDesc,
257
+ label: 'SSGI:moments'
258
+ });
259
+ const ao = builder.createTexture({
260
+ ...aoTraceDesc,
261
+ label: 'SSGI:temporalAO'
262
+ });
263
+ const fb = builder.createFramebuffer({
264
+ label: 'SSGI:temporalFB',
265
+ width: traceWidth,
266
+ height: traceHeight,
267
+ colorAttachments: [
268
+ irradiance,
269
+ moments,
270
+ ao
271
+ ],
272
+ depthAttachment: null,
273
+ ignoreDepthStencil: true
274
+ });
275
+ builder.setExecute((rg)=>{
276
+ const device = ctx.device;
277
+ device.pushDeviceStates();
278
+ try {
279
+ device.setFramebuffer(rg.getFramebuffer(fb));
280
+ this.temporal(ctx, rg.getTexture(traceResult.irradiance), rg.getTexture(traceResult.ao), rg.getTexture(linearDepthHandle), rg.getTexture(normalHandle), canTemporal ? rg.getTexture(motionHandle) : null, canTemporal ? rg.getTexture(previousIrradianceHandle) : null, canTemporal ? rg.getTexture(previousSurfaceHandle) : null, canTemporal ? rg.getTexture(previousMomentsHandle) : null, canTemporal ? rg.getTexture(previousAOHandle) : null, traceWidth, traceHeight);
281
+ } finally{
282
+ device.popDeviceStates();
283
+ }
284
+ });
285
+ return {
286
+ irradiance,
287
+ moments,
288
+ ao
289
+ };
290
+ });
291
+ // Variance-guided, cross-bilateral a-trous filtering. The increasing
292
+ // kernel step removes low-frequency Monte Carlo noise while depth/normal
293
+ // weights preserve discontinuities.
294
+ let filteredHandle = temporalResult.irradiance;
295
+ let filteredAOHandle = temporalResult.ao;
296
+ for(let pass = 0; pass < settings.denoisePasses; pass++){
297
+ const source = filteredHandle;
298
+ const aoSource = filteredAOHandle;
299
+ const filtered = graph.addPass(`SSGI:ATrous:${pass}`, (builder)=>{
300
+ builder.read(source);
301
+ builder.read(aoSource);
302
+ builder.read(temporalResult.moments);
303
+ builder.read(linearDepthHandle);
304
+ builder.read(normalHandle);
159
305
  const out = builder.createTexture({
160
- ...texDesc,
161
- label: 'SSGI:temporal'
306
+ ...traceDesc,
307
+ label: `SSGI:atrous${pass}`
308
+ });
309
+ const ao = builder.createTexture({
310
+ ...aoTraceDesc,
311
+ label: `SSGI:atrousAO${pass}`
162
312
  });
163
313
  const fb = builder.createFramebuffer({
164
- label: 'SSGI:temporalFB',
314
+ label: `SSGI:atrousFB${pass}`,
165
315
  width: traceWidth,
166
316
  height: traceHeight,
167
- colorAttachments: out,
317
+ colorAttachments: [
318
+ out,
319
+ ao
320
+ ],
168
321
  depthAttachment: null,
169
322
  ignoreDepthStencil: true
170
323
  });
@@ -172,44 +325,48 @@ import { FrameResources } from '../render/rendergraph/blackboard.js';
172
325
  const device = ctx.device;
173
326
  device.pushDeviceStates();
174
327
  try {
175
- this.temporal(ctx, rg.getTexture(traceHandle), rg.getTexture(prevRadianceHandle), rg.getTexture(prevMotionVectorHandle), rg.getFramebuffer(fb));
328
+ device.setFramebuffer(rg.getFramebuffer(fb));
329
+ this.atrous(ctx, rg.getTexture(source), rg.getTexture(aoSource), rg.getTexture(temporalResult.moments), rg.getTexture(linearDepthHandle), rg.getTexture(normalHandle), traceWidth, traceHeight, 1 << pass, canTemporal);
176
330
  } finally{
177
331
  device.popDeviceStates();
178
332
  }
179
333
  });
180
- return out;
334
+ return {
335
+ irradiance: out,
336
+ ao
337
+ };
181
338
  });
339
+ filteredHandle = filtered.irradiance;
340
+ filteredAOHandle = filtered.ao;
182
341
  }
183
- // The accumulated (pre-blur) signal is what feeds next frame's history.
184
- const accumulatedHandle = radianceHandle;
185
- // 3. Bilateral blur (horizontal + vertical) guided by scene depth cleans
186
- // the residual noise left after accumulation.
187
- if (camera.ssgiBlurKernelSize > 0) {
188
- const blurInputHandle = radianceHandle;
189
- radianceHandle = graph.addPass('SSGI:Blur', (builder)=>{
190
- builder.read(blurInputHandle);
191
- readCommon(builder);
192
- const middle = builder.createTexture({
193
- ...texDesc,
194
- label: 'SSGI:blurH'
195
- });
342
+ // Half-resolution presets use a joint bilateral upsample before history is
343
+ // consumed by full-resolution materials on the next frame.
344
+ let finalIrradianceHandle = filteredHandle;
345
+ let finalAOHandle = filteredAOHandle;
346
+ if (settings.halfRes) {
347
+ const source = filteredHandle;
348
+ const aoSource = filteredAOHandle;
349
+ const upsampled = graph.addPass('SSGI:Upsample', (builder)=>{
350
+ builder.read(source);
351
+ builder.read(aoSource);
352
+ builder.read(linearDepthHandle);
353
+ builder.read(normalHandle);
196
354
  const out = builder.createTexture({
197
- ...texDesc,
198
- label: 'SSGI:blur'
355
+ ...fullIrradianceDesc,
356
+ label: 'SSGI:upsampledIrradiance'
199
357
  });
200
- const middleFB = builder.createFramebuffer({
201
- label: 'SSGI:blurHFB',
202
- width: traceWidth,
203
- height: traceHeight,
204
- colorAttachments: middle,
205
- depthAttachment: null,
206
- ignoreDepthStencil: true
358
+ const ao = builder.createTexture({
359
+ ...fullAODesc,
360
+ label: 'SSGI:upsampledAO'
207
361
  });
208
- const outFB = builder.createFramebuffer({
209
- label: 'SSGI:blurFB',
210
- width: traceWidth,
211
- height: traceHeight,
212
- colorAttachments: out,
362
+ const fb = builder.createFramebuffer({
363
+ label: 'SSGI:upsampleFB',
364
+ width: s.width,
365
+ height: s.height,
366
+ colorAttachments: [
367
+ out,
368
+ ao
369
+ ],
213
370
  depthAttachment: null,
214
371
  ignoreDepthStencil: true
215
372
  });
@@ -217,73 +374,68 @@ import { FrameResources } from '../render/rendergraph/blackboard.js';
217
374
  const device = ctx.device;
218
375
  device.pushDeviceStates();
219
376
  try {
220
- const depthTex = getLinearDepth(rg);
221
- const blitterH = SSGI._blurBlitterH = SSGI._blurBlitterH ?? new BilateralBlurBlitter(false);
222
- this.blurPass(ctx, blitterH, depthTex, rg.getTexture(blurInputHandle), rg.getFramebuffer(middleFB));
223
- const blitterV = SSGI._blurBlitterV = SSGI._blurBlitterV ?? new BilateralBlurBlitter(true);
224
- this.blurPass(ctx, blitterV, depthTex, rg.getTexture(middle), rg.getFramebuffer(outFB));
377
+ device.setFramebuffer(rg.getFramebuffer(fb));
378
+ this.upsample(ctx, rg.getTexture(source), rg.getTexture(aoSource), rg.getTexture(linearDepthHandle), rg.getTexture(normalHandle));
225
379
  } finally{
226
380
  device.popDeviceStates();
227
381
  }
228
382
  });
229
- return out;
383
+ return {
384
+ irradiance: out,
385
+ ao
386
+ };
230
387
  });
388
+ finalIrradianceHandle = upsampled.irradiance;
389
+ finalAOHandle = upsampled.ao;
231
390
  }
232
- // 4. Upsample, modulate by the albedo approximation and add to the scene
233
- // color; queue history commits.
234
- const finalRadianceHandle = radianceHandle;
235
- return graph.addPass('PostEffect:SSGI', (builder)=>{
391
+ // Pinned before the commit closure captures it: the a-trous loop above
392
+ // reassigns the handle, and the AO history has to be the trace-resolution
393
+ // one regardless of whether an upsample ran.
394
+ const traceResolutionAOHandle = filteredAOHandle;
395
+ // SSGI history is independent from TAA/SSR history. Commit the opaque HDR
396
+ // SceneColor input (before SSR/transparency/tone-map/AA), irradiance,
397
+ // surface and moments, then pass SceneColor through unchanged.
398
+ return graph.addPass('SSGI:Commit', (builder)=>{
236
399
  builder.read(s.input);
237
- builder.read(finalRadianceHandle);
238
- if (accumulatedHandle !== finalRadianceHandle) {
239
- builder.read(accumulatedHandle);
240
- }
241
- if (motionVectorHandle) {
242
- builder.read(motionVectorHandle);
400
+ if (noFogHandle) {
401
+ builder.read(noFogHandle);
243
402
  }
244
- readCommon(builder);
245
- const output = s.createOutput(builder, {
246
- needDepthAttachment: true
247
- });
403
+ builder.read(finalIrradianceHandle);
404
+ builder.read(finalAOHandle);
405
+ // The trace-resolution AO is what goes back into history, so it has to be
406
+ // read here too even when the upsampled one is what gets composited.
407
+ builder.read(traceResolutionAOHandle);
408
+ builder.read(surfaceHandle);
409
+ builder.read(temporalResult.moments);
410
+ const output = s.createOutput(builder);
248
411
  builder.setExecute((rg)=>{
249
412
  const device = ctx.device;
250
413
  device.pushDeviceStates();
251
414
  try {
252
415
  device.setFramebuffer(output.framebuffer ? rg.getFramebuffer(output.framebuffer) : null);
253
- const inputTex = rg.getTexture(s.input);
254
- const radianceTex = rg.getTexture(finalRadianceHandle);
255
- copyTexture(inputTex, device.getFramebuffer(), fetchSampler('clamp_nearest_nomip'), AbstractPostEffect.getDefaultRenderState(ctx, 'eq'));
256
- this.combine(ctx, inputTex, radianceTex, output.srgbOutput);
257
- // Commit history whenever temporal is wanted - even on frames where
258
- // the temporal pass could not run yet (no compatible history). This
259
- // seeds the very first frame; gating the commit on canTemporal
260
- // would deadlock: no history -> no temporal -> no commit -> no
261
- // history, forever.
262
- if (wantTemporal && history && motionVectorHandle) {
263
- // Commit the PRE-blur accumulated signal: feeding the blurred
264
- // result back would compound the blur every frame.
265
- const accumulatedTex = rg.getTexture(accumulatedHandle);
266
- history.queueRetainedCommit(RGHistoryResources.SSGI_RADIANCE, {
267
- format: accumulatedTex.format,
268
- sizeMode: 'absolute',
269
- width: accumulatedTex.width,
270
- height: accumulatedTex.height
271
- }, {
272
- width: accumulatedTex.width,
273
- height: accumulatedTex.height
274
- }, accumulatedTex);
275
- if (ctx.motionVectorTexture) {
276
- history.queueRetainedCommit(RGHistoryResources.SSGI_MOTION_VECTOR, {
277
- format: ctx.motionVectorTexture.format,
278
- sizeMode: 'absolute',
279
- width: ctx.motionVectorTexture.width,
280
- height: ctx.motionVectorTexture.height
281
- }, {
282
- width: ctx.motionVectorTexture.width,
283
- height: ctx.motionVectorTexture.height
284
- }, ctx.motionVectorTexture);
285
- }
416
+ const sceneColor = rg.getTexture(s.input);
417
+ const irradiance = rg.getTexture(finalIrradianceHandle);
418
+ const ao = rg.getTexture(finalAOHandle);
419
+ const filteredAO = rg.getTexture(traceResolutionAOHandle);
420
+ const surface = rg.getTexture(surfaceHandle);
421
+ const moments = rg.getTexture(temporalResult.moments);
422
+ if (ctx.camera.ssgiAOIntensity > 0) {
423
+ this.compositeAO(ctx, sceneColor, ao, output.srgbOutput);
424
+ } else {
425
+ this.passThrough(ctx, sceneColor, output.srgbOutput);
426
+ }
427
+ if (ctx.device.type === 'webgpu') {
428
+ // Commit the same fog-free color the trace sampled, so next frame's
429
+ // reprojected history is on the same footing as this frame's fallback.
430
+ this.commitHistory(history, RGHistoryResources.SSGI_SCENE_COLOR, rg.getTexture(sampleColorHandle));
286
431
  }
432
+ this.commitHistory(history, RGHistoryResources.SSGI_IRRADIANCE, irradiance);
433
+ this.commitHistory(history, RGHistoryResources.SSGI_SURFACE, surface);
434
+ this.commitHistory(history, RGHistoryResources.SSGI_MOMENTS, moments);
435
+ // Committed at trace resolution: the temporal pass that consumes it
436
+ // runs before the upsample, so a full-resolution AO would fail the
437
+ // descriptor compatibility check and silently disable accumulation.
438
+ this.commitHistory(history, RGHistoryResources.SSGI_AO, filteredAO);
287
439
  } finally{
288
440
  device.popDeviceStates();
289
441
  }
@@ -291,116 +443,180 @@ import { FrameResources } from '../render/rendergraph/blackboard.js';
291
443
  return output.color;
292
444
  });
293
445
  }
294
- /** @internal */ blurPass(ctx, blitter, depthTex, srcTex, fbTo) {
295
- const camera = ctx.camera;
296
- blitter.kernelRadius = Math.max(1, camera.ssgiBlurKernelSize >> 0) - 1 >> 1;
297
- blitter.stdDev = camera.ssgiBlurStdDev;
298
- blitter.size = new Vector2(srcTex.width, srcTex.height);
299
- blitter.depthTex = depthTex;
300
- blitter.depthCutoff = camera.ssgiBlurDepthCutoff;
301
- blitter.blurSizeTex = null;
302
- blitter.sampler = fetchSampler('clamp_nearest_nomip');
303
- blitter.cameraNearFar.setXY(camera.getNearPlane(), camera.getFarPlane());
304
- blitter.srgbOut = false;
305
- blitter.blit(srcTex, fbTo, fetchSampler('clamp_linear_nomip'));
446
+ /** @internal */ commitHistory(history, name, texture) {
447
+ history.queueRetainedCommit(name, {
448
+ format: texture.format,
449
+ sizeMode: 'absolute',
450
+ width: texture.width,
451
+ height: texture.height
452
+ }, {
453
+ width: texture.width,
454
+ height: texture.height
455
+ }, texture);
306
456
  }
307
- /** @internal */ trace(ctx, inputColorTexture, sceneDepthTexture) {
308
- const device = ctx.device;
309
- const hash = `${!!ctx.HiZTexture}:${!!ctx.SSRCalcThickness}`;
310
- let program = SSGI._tracePrograms[hash];
311
- if (program === undefined) {
312
- const created = this._createTraceProgram(ctx);
313
- if (!created) {
314
- return;
457
+ /** @internal */ trace(ctx, currentColor, previousColor, depth, normal, motion, previousSurface, hiZ, width, height) {
458
+ const sampleHistory = !!(previousColor && motion && previousSurface);
459
+ const envHash = ctx.env.light.getHash();
460
+ const historyHash = sampleHistory ? `${previousColor.uid}:${motion.uid}:${previousSurface.uid}` : '';
461
+ const hizHash = hiZ ? `${hiZ.uid}` : '';
462
+ const programHash = `${sampleHistory ? '1' : '0'}:${hiZ ? '1' : '0'}:${ctx.camera.ssgiResolvedSettings.raysPerPixel}:${envHash}`;
463
+ const bindGroupHash = `${currentColor.uid}:${depth.uid}:${normal.uid}:(${historyHash}):(${hizHash}):${ctx.camera.ssgiResolvedSettings.raysPerPixel}:${envHash}`;
464
+ let program = SSGI._tracePrograms[programHash];
465
+ if (!program) {
466
+ program = this.createTraceProgram(ctx, !!hiZ, sampleHistory);
467
+ SSGI._tracePrograms[programHash] = program;
468
+ }
469
+ const colorSampler = fetchSampler(ctx.device.type === 'webgl' ? 'clamp_nearest_nomip' : 'clamp_linear_nomip');
470
+ let bindGroup = this._traceBindGroups[bindGroupHash];
471
+ if (!bindGroup) {
472
+ bindGroup = ctx.device.createBindGroup(program.bindGroupLayouts[0]);
473
+ bindGroup.setTexture('currentColorTex', currentColor, colorSampler);
474
+ bindGroup.setTexture('depthTex', depth, fetchSampler('clamp_nearest_nomip'));
475
+ bindGroup.setTexture('normalTex', normal, fetchSampler('clamp_nearest_nomip'));
476
+ if (sampleHistory) {
477
+ bindGroup.setTexture('previousColorTex', previousColor, colorSampler);
478
+ bindGroup.setTexture('motionTex', motion, fetchSampler('clamp_nearest_nomip'));
479
+ bindGroup.setTexture('previousSurfaceTex', previousSurface, fetchSampler('clamp_nearest_nomip'));
315
480
  }
316
- program = created;
317
- SSGI._tracePrograms[hash] = program;
481
+ if (hiZ) {
482
+ bindGroup.setTexture('hizTex', hiZ, fetchSampler('clamp_nearest'));
483
+ bindGroup.setValue('depthMipLevels', hiZ.mipLevelCount);
484
+ }
485
+ this._traceBindGroups[bindGroupHash] = bindGroup;
486
+ }
487
+ if (sampleHistory) {
488
+ bindGroup.setValue('historyRejectParams', new Vector2(ctx.camera.ssgiDepthReject, ctx.camera.ssgiNormalReject));
318
489
  }
319
- let bindGroup = this._traceBindGroups[hash];
490
+ bindGroup.setValue('cameraNearFar', new Vector2(ctx.camera.getNearPlane(), ctx.camera.getFarPlane()));
491
+ bindGroup.setValue('viewMatrix', ctx.camera.viewMatrix);
492
+ bindGroup.setValue('invViewMatrix', ctx.camera.worldMatrix);
493
+ bindGroup.setValue('projMatrix', ctx.camera.getProjectionMatrix());
494
+ bindGroup.setValue('invProjMatrix', Matrix4x4.invert(ctx.camera.getProjectionMatrix()));
495
+ bindGroup.setValue('traceParams', new Vector4(ctx.camera.ssgiMaxDistance, ctx.camera.ssgiThickness, ctx.camera.ssgiResolvedSettings.maxSteps, ctx.camera.ssgiStride));
496
+ bindGroup.setValue('targetSize', new Vector4(width, height, hiZ?.width ?? depth.width, hiZ?.height ?? depth.height));
497
+ bindGroup.setValue('radianceParams', new Vector4(ctx.camera.ssgiIntensity, ctx.camera.ssgiMaxRayIntensity, ShaderHelper.getEnvLightLuminance(ctx), ctx.device.frameInfo.frameCounter));
498
+ bindGroup.setValue('skyOcclusion', ctx.camera.ssgiSkyOcclusion);
499
+ bindGroup.setValue('flip', this.needFlip(ctx.device) ? 1 : 0);
500
+ ctx.env.light.envLight.updateBindGroup(bindGroup);
501
+ ctx.device.setProgram(program);
502
+ ctx.device.setBindGroup(0, bindGroup);
503
+ this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
504
+ }
505
+ /** @internal */ temporal(ctx, current, currentAO, depth, normal, motion, previousIrradiance, previousSurface, previousMoments, previousAO, width, height) {
506
+ const hasHistory = !!(motion && previousIrradiance && previousSurface && previousMoments && previousAO);
507
+ const hash = hasHistory ? `history:${current.uid}:${currentAO.uid}:${depth.uid}:${normal.uid}:${motion.uid}:${previousIrradiance.uid}:${previousSurface.uid}:${previousMoments.uid}:${previousAO.uid}` : `initialize:${current.uid}:${currentAO.uid}:${depth.uid}:${normal.uid}`;
508
+ let program = SSGI._temporalPrograms[hash];
509
+ if (!program) {
510
+ program = this.createTemporalProgram(ctx, hasHistory);
511
+ SSGI._temporalPrograms[hash] = program;
512
+ }
513
+ let bindGroup = this._temporalBindGroups[hash];
320
514
  if (!bindGroup) {
321
- bindGroup = device.createBindGroup(program.bindGroupLayouts[0]);
322
- this._traceBindGroups[hash] = bindGroup;
515
+ bindGroup = ctx.device.createBindGroup(program.bindGroupLayouts[0]);
516
+ bindGroup.setTexture('currentTex', current, fetchSampler('clamp_nearest_nomip'));
517
+ bindGroup.setTexture('currentAOTex', currentAO, fetchSampler('clamp_nearest_nomip'));
518
+ bindGroup.setTexture('depthTex', depth, fetchSampler('clamp_nearest_nomip'));
519
+ bindGroup.setTexture('normalTex', normal, fetchSampler('clamp_nearest_nomip'));
520
+ if (hasHistory) {
521
+ bindGroup.setTexture('motionTex', motion, fetchSampler('clamp_nearest_nomip'));
522
+ bindGroup.setTexture('previousIrradianceTex', previousIrradiance, fetchSampler('clamp_linear_nomip'));
523
+ bindGroup.setTexture('previousSurfaceTex', previousSurface, fetchSampler('clamp_nearest_nomip'));
524
+ bindGroup.setTexture('previousMomentsTex', previousMoments, fetchSampler('clamp_linear_nomip'));
525
+ // Linear is safe here: this branch only runs on WebGPU, where r16f is
526
+ // unconditionally filterable, and the read is at a reprojected UV.
527
+ bindGroup.setTexture('previousAOTex', previousAO, fetchSampler('clamp_linear_nomip'));
528
+ }
529
+ this._temporalBindGroups[hash] = bindGroup;
323
530
  }
324
- const camera = ctx.camera;
325
- const nearestSampler = fetchSampler('clamp_nearest');
326
- const linearSampler = fetchSampler('clamp_linear');
327
- const fb = device.getFramebuffer();
328
- const traceWidth = fb.getWidth();
329
- const traceHeight = fb.getHeight();
330
- bindGroup.setTexture('colorTex', inputColorTexture, linearSampler);
331
- bindGroup.setTexture('normalTex', ctx.SSRNormalTexture, nearestSampler);
332
- bindGroup.setTexture('depthTex', sceneDepthTexture, nearestSampler);
333
- bindGroup.setValue('cameraNearFar', new Vector2(camera.getNearPlane(), camera.getFarPlane()));
334
- bindGroup.setValue('projMatrix', camera.getProjectionMatrix());
335
- bindGroup.setValue('invProjMatrix', Matrix4x4.invert(camera.getProjectionMatrix()));
336
- bindGroup.setValue('viewMatrix', camera.viewMatrix);
337
- bindGroup.setValue('ssgiParams', new Vector4(camera.ssgiRadius, camera.ssgiMaxSteps, camera.ssgiThickness, camera.ssgiSamples));
338
- // Advance the noise sequence only when the temporal resolve can converge
339
- // it; a static pattern is less objectionable than unaccumulated flicker.
340
- bindGroup.setValue('frameIndex', camera.ssgiTemporal ? device.frameInfo.frameCounter % 1024 : 0);
341
- if (ctx.HiZTexture) {
342
- bindGroup.setTexture('hizTex', ctx.HiZTexture, nearestSampler);
343
- bindGroup.setValue('depthMipLevels', ctx.HiZTexture.mipLevelCount);
344
- bindGroup.setValue('targetSize', new Vector4(traceWidth, traceHeight, ctx.HiZTexture.width, ctx.HiZTexture.height));
345
- } else {
346
- bindGroup.setValue('ssgiStride', camera.ssgiStride);
347
- bindGroup.setValue('targetSize', new Vector4(traceWidth, traceHeight, sceneDepthTexture.width, sceneDepthTexture.height));
531
+ bindGroup.setValue('targetSize', new Vector4(width, height, depth.width, depth.height));
532
+ bindGroup.setValue('temporalParams', new Vector4(ctx.camera.ssgiTemporalWeight, ctx.camera.ssgiDepthReject, ctx.camera.ssgiNormalReject, ctx.camera.getFarPlane()));
533
+ bindGroup.setValue('flip', this.needFlip(ctx.device) ? 1 : 0);
534
+ ctx.device.setProgram(program);
535
+ ctx.device.setBindGroup(0, bindGroup);
536
+ this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
537
+ }
538
+ /** @internal */ atrous(ctx, source, aoSource, moments, depth, normal, width, height, step, hasTemporalHistory) {
539
+ let program = SSGI._atrousProgram;
540
+ if (!program) {
541
+ program = this.createAtrousProgram(ctx);
542
+ SSGI._atrousProgram = program;
543
+ }
544
+ const hash = `atrous:${source.uid}:${aoSource.uid}:${moments.uid}:${depth.uid}:${normal.uid}`;
545
+ let bindGroup = this._atrousBindGroups[hash];
546
+ if (!bindGroup) {
547
+ bindGroup = ctx.device.createBindGroup(program.bindGroupLayouts[0]);
548
+ bindGroup.setTexture('sourceTex', source, fetchSampler('clamp_nearest_nomip'));
549
+ bindGroup.setTexture('aoSourceTex', aoSource, fetchSampler('clamp_nearest_nomip'));
550
+ bindGroup.setTexture('momentsTex', moments, fetchSampler('clamp_nearest_nomip'));
551
+ bindGroup.setTexture('depthTex', depth, fetchSampler('clamp_nearest_nomip'));
552
+ bindGroup.setTexture('normalTex', normal, fetchSampler('clamp_nearest_nomip'));
553
+ this._atrousBindGroups[hash] = bindGroup;
348
554
  }
349
- bindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
350
- device.setProgram(program);
351
- device.setBindGroup(0, bindGroup);
555
+ bindGroup.setValue('targetSize', new Vector4(width, height, depth.width, depth.height));
556
+ bindGroup.setValue('filterParams', new Vector4(step, Math.max(0.001, ctx.camera.ssgiDepthReject), Math.max(1, ctx.camera.ssgiNormalReject * 32), 4));
557
+ // Number of accumulated frames a pixel needs before its variance estimate is
558
+ // trusted. Zero disables the ramp for paths that never build history, where
559
+ // the stored length is pinned at 1 and would read as permanently new.
560
+ bindGroup.setValue('denoiseParams', new Vector4(hasTemporalHistory ? 8 : 0, 0, 0, 0));
561
+ bindGroup.setValue('cameraFar', ctx.camera.getFarPlane());
562
+ bindGroup.setValue('flip', this.needFlip(ctx.device) ? 1 : 0);
563
+ ctx.device.setProgram(program);
564
+ ctx.device.setBindGroup(0, bindGroup);
352
565
  this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
353
566
  }
354
- /** @internal */ temporal(ctx, currentRadianceTex, prevRadianceTex, prevMotionVectorTex, outFramebuffer) {
355
- const device = ctx.device;
356
- let program = SSGI._temporalProgram;
567
+ /** @internal */ writeSurface(ctx, depth, normal) {
568
+ let program = SSGI._surfaceProgram;
357
569
  if (!program) {
358
- program = this._createTemporalProgram(ctx);
359
- SSGI._temporalProgram = program;
570
+ program = this.createSurfaceProgram(ctx);
571
+ SSGI._surfaceProgram = program;
360
572
  }
361
- if (!this._temporalBindGroup) {
362
- this._temporalBindGroup = device.createBindGroup(program.bindGroupLayouts[0]);
573
+ const hash = `${depth.uid}:${normal.uid}`;
574
+ let bindGroup = this._surfaceBindGroups[hash];
575
+ if (!bindGroup) {
576
+ bindGroup = ctx.device.createBindGroup(program.bindGroupLayouts[0]);
577
+ bindGroup.setTexture('depthTex', depth, fetchSampler('clamp_nearest_nomip'));
578
+ bindGroup.setTexture('normalTex', normal, fetchSampler('clamp_nearest_nomip'));
579
+ bindGroup.setValue('targetSize', new Vector2(depth.width, depth.height));
580
+ this._surfaceBindGroups[hash] = bindGroup;
363
581
  }
364
- this._temporalBindGroup.setTexture('historyColorTex', prevRadianceTex, fetchSampler('clamp_linear_nomip'));
365
- this._temporalBindGroup.setTexture('currentColorTex', currentRadianceTex, fetchSampler('clamp_nearest_nomip'));
366
- this._temporalBindGroup.setTexture('motionVector', ctx.motionVectorTexture, fetchSampler('clamp_linear_nomip'));
367
- this._temporalBindGroup.setTexture('prevMotionVector', prevMotionVectorTex, fetchSampler('clamp_linear_nomip'));
368
- this._temporalBindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
369
- this._temporalBindGroup.setValue('texSize', new Vector2(currentRadianceTex.width, currentRadianceTex.height));
370
- this._temporalBindGroup.setValue('temporalWeight', ctx.camera.ssgiTemporalWeight);
371
- device.setFramebuffer(outFramebuffer);
372
- device.setProgram(program);
373
- device.setBindGroup(0, this._temporalBindGroup);
582
+ bindGroup.setValue('cameraFar', ctx.camera.getFarPlane());
583
+ bindGroup.setValue('flip', this.needFlip(ctx.device) ? 1 : 0);
584
+ ctx.device.setProgram(program);
585
+ ctx.device.setBindGroup(0, bindGroup);
374
586
  this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
375
587
  }
376
- /** @internal */ combine(ctx, inputColorTexture, radianceTex, srgbOut) {
377
- const device = ctx.device;
378
- let program = SSGI._combineProgram;
379
- if (program === undefined) {
380
- program = this._createCombineProgram(ctx);
381
- SSGI._combineProgram = program;
588
+ /** @internal */ upsample(ctx, source, aoSource, depth, normal) {
589
+ let program = SSGI._upsampleProgram;
590
+ if (!program) {
591
+ program = this.createUpsampleProgram(ctx);
592
+ SSGI._upsampleProgram = program;
382
593
  }
383
- if (!this._combineBindGroup) {
384
- this._combineBindGroup = device.createBindGroup(program.bindGroupLayouts[0]);
594
+ const hash = `${source.uid}:${aoSource.uid}:${depth.uid}:${normal.uid}`;
595
+ let bindGroup = this._upsampleBindGroups[hash];
596
+ if (!bindGroup) {
597
+ bindGroup = ctx.device.createBindGroup(program.bindGroupLayouts[0]);
598
+ bindGroup.setTexture('sourceTex', source, fetchSampler('clamp_nearest_nomip'));
599
+ bindGroup.setTexture('aoSourceTex', aoSource, fetchSampler('clamp_nearest_nomip'));
600
+ bindGroup.setTexture('depthTex', depth, fetchSampler('clamp_nearest_nomip'));
601
+ bindGroup.setTexture('normalTex', normal, fetchSampler('clamp_nearest_nomip'));
602
+ bindGroup.setValue('targetSize', new Vector4(source.width, source.height, depth.width, depth.height));
603
+ this._upsampleBindGroups[hash] = bindGroup;
385
604
  }
386
- this._combineBindGroup.setTexture('colorTex', inputColorTexture, fetchSampler('clamp_nearest'));
387
- this._combineBindGroup.setTexture('radianceTex', radianceTex, fetchSampler('clamp_linear'));
388
- this._combineBindGroup.setValue('ssgiIntensity', ctx.camera.ssgiIntensity);
389
- this._combineBindGroup.setValue('targetSize', new Vector2(inputColorTexture.width, inputColorTexture.height));
390
- this._combineBindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
391
- this._combineBindGroup.setValue('srgbOut', srgbOut ? 1 : 0);
392
- device.setProgram(program);
393
- device.setBindGroup(0, this._combineBindGroup);
394
- this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'gt'));
605
+ bindGroup.setValue('guideParams', new Vector4(Math.max(0.001, ctx.camera.ssgiDepthReject), Math.max(1, ctx.camera.ssgiNormalReject * 32), ctx.camera.getFarPlane(), 0));
606
+ bindGroup.setValue('flip', this.needFlip(ctx.device) ? 1 : 0);
607
+ ctx.device.setProgram(program);
608
+ ctx.device.setBindGroup(0, bindGroup);
609
+ this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
395
610
  }
396
- /** @internal */ _createTraceProgram(ctx) {
611
+ /** @internal */ createTraceProgram(ctx, useHiZ, sampleHistory) {
612
+ const raysPerPixel = ctx.camera.ssgiResolvedSettings.raysPerPixel;
397
613
  const program = ctx.device.buildRenderProgram({
398
614
  vertex (pb) {
399
615
  this.flip = pb.int().uniform(0);
400
616
  this.$inputs.pos = pb.vec2().attrib('position');
401
617
  this.$outputs.uv = pb.vec2();
402
618
  pb.main(function() {
403
- this.$builtins.position = pb.vec4(this.$inputs.pos, 1, 1);
619
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
404
620
  this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
405
621
  this.$if(pb.notEqual(this.flip, 0), function() {
406
622
  this.$builtins.position.y = pb.neg(this.$builtins.position.y);
@@ -408,100 +624,208 @@ import { FrameResources } from '../render/rendergraph/blackboard.js';
408
624
  });
409
625
  },
410
626
  fragment (pb) {
411
- this.colorTex = pb.tex2D().uniform(0);
627
+ this.currentColorTex = pb.tex2D().uniform(0);
628
+ // Linear depth is r32f (or rg32f when thickness is enabled). These
629
+ // formats are not filterable without the optional float32-filterable
630
+ // feature, and all SSGI depth reads use a nearest sampler.
631
+ this.depthTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
412
632
  this.normalTex = pb.tex2D().uniform(0);
413
- this.depthTex = pb.tex2D().uniform(0);
633
+ if (sampleHistory) {
634
+ this.previousColorTex = pb.tex2D().uniform(0);
635
+ this.motionTex = pb.tex2D().uniform(0);
636
+ this.previousSurfaceTex = pb.tex2D().uniform(0);
637
+ this.historyRejectParams = pb.vec2().uniform(0);
638
+ }
639
+ if (useHiZ) {
640
+ // Hi-Z is stored as r32f. It is only read with a nearest sampler,
641
+ // so use the unfilterable sample type for adapters without the
642
+ // optional float32-filterable feature (notably some AMD iGPUs).
643
+ this.hizTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
644
+ this.depthMipLevels = pb.int().uniform(0);
645
+ }
414
646
  this.cameraNearFar = pb.vec2().uniform(0);
647
+ this.viewMatrix = pb.mat4().uniform(0);
648
+ this.invViewMatrix = pb.mat4().uniform(0);
415
649
  this.projMatrix = pb.mat4().uniform(0);
416
650
  this.invProjMatrix = pb.mat4().uniform(0);
417
- this.viewMatrix = pb.mat4().uniform(0);
418
- // (radius, maxIterations, thickness, numRays)
419
- this.ssgiParams = pb.vec4().uniform(0);
420
- this.frameIndex = pb.float().uniform(0);
651
+ this.traceParams = pb.vec4().uniform(0);
421
652
  this.targetSize = pb.vec4().uniform(0);
422
- if (ctx.HiZTexture) {
423
- this.hizTex = pb.tex2D().uniform(0);
424
- this.depthMipLevels = pb.int().uniform(0);
425
- } else {
426
- this.ssgiStride = pb.float().uniform(0);
427
- }
653
+ this.radianceParams = pb.vec4().uniform(0);
654
+ this.skyOcclusion = pb.float().uniform(0);
655
+ ctx.env.light.envLight.initShaderBindings(pb);
428
656
  this.$outputs.outColor = pb.vec4();
429
- pb.func('getPosition', [
430
- pb.vec2('uv'),
431
- pb.mat4('mat')
657
+ this.$outputs.outAO = pb.vec4();
658
+ pb.func('SSGI_getPosition', [
659
+ pb.vec2('uv')
432
660
  ], function() {
433
661
  this.$l.linearDepth = ShaderHelper.sampleLinearDepth(this, this.depthTex, this.uv, 0);
434
- this.$l.nonLinearDepth = pb.div(pb.sub(pb.div(this.cameraNearFar.x, this.linearDepth), this.cameraNearFar.y), pb.sub(this.cameraNearFar.x, this.cameraNearFar.y));
435
- this.$l.clipSpacePos = pb.vec4(pb.sub(pb.mul(this.uv, 2), pb.vec2(1)), pb.sub(pb.mul(pb.clamp(this.nonLinearDepth, 0, 1), 2), 1), 1);
436
- this.$l.wPos = pb.mul(this.mat, this.clipSpacePos);
437
- this.$return(pb.vec4(pb.div(this.wPos.xyz, this.wPos.w), this.linearDepth));
662
+ this.$l.nonLinearDepth = ShaderHelper.linearNormalizedToNonLinearDepth(this, this.linearDepth, this.cameraNearFar);
663
+ this.$l.clipPos = pb.vec4(pb.sub(pb.mul(this.uv, 2), pb.vec2(1)), ShaderHelper.deviceDepthToClipZ(this, pb.clamp(this.nonLinearDepth, 0, 1)), 1);
664
+ this.$l.viewPos = pb.mul(this.invProjMatrix, this.clipPos);
665
+ this.$return(pb.vec4(pb.div(this.viewPos.xyz, this.viewPos.w), this.linearDepth));
666
+ });
667
+ pb.func('SSGI_hash22', [
668
+ pb.vec2('p'),
669
+ pb.float('seed')
670
+ ], function() {
671
+ this.$l.q = pb.vec2(pb.dot(this.p, pb.vec2(127.1, 311.7)), pb.dot(this.p, pb.vec2(269.5, 183.3)));
672
+ this.$return(pb.fract(pb.mul(pb.sin(pb.add(this.q, this.seed)), 43758.5453)));
673
+ });
674
+ pb.func('SSGI_cosineDirection', [
675
+ pb.vec3('n'),
676
+ pb.vec2('xi')
677
+ ], function() {
678
+ this.$l.up = this.$choice(pb.lessThan(pb.abs(this.n.z), 0.999), pb.vec3(0, 0, 1), pb.vec3(1, 0, 0));
679
+ this.$l.tangent = pb.normalize(pb.cross(this.up, this.n));
680
+ this.$l.bitangent = pb.cross(this.n, this.tangent);
681
+ this.$l.r = pb.sqrt(this.xi.x);
682
+ this.$l.phi = pb.mul(2 * Math.PI, this.xi.y);
683
+ this.$l.local = pb.vec3(pb.mul(this.r, pb.cos(this.phi)), pb.mul(this.r, pb.sin(this.phi)), pb.sqrt(pb.max(0, pb.sub(1, this.xi.x))));
684
+ this.$return(pb.normalize(pb.add(pb.mul(this.tangent, this.local.x), pb.mul(this.bitangent, this.local.y), pb.mul(this.n, this.local.z))));
438
685
  });
439
686
  pb.main(function() {
440
- this.$l.screenUV = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize.xy);
441
- this.$l.color = pb.vec3(0);
442
- this.$l.pos = this.getPosition(this.screenUV, this.invProjMatrix);
443
- this.$if(pb.lessThan(this.pos.w, 1), function() {
444
- this.$l.viewPos = this.pos.xyz;
445
- this.$l.worldNormal = pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.screenUV, 0).rgb, 2), pb.vec3(1));
687
+ this.$l.uv = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize.xy);
688
+ this.$l.pos = this.SSGI_getPosition(this.uv);
689
+ this.$if(pb.greaterThanEqual(this.pos.w, 1), function() {
690
+ this.$outputs.outColor = pb.vec4(0);
691
+ // Sky is unoccluded. Its irradiance validity is zero, so this value
692
+ // is never read as a weight, but it must not be a dark texel: the
693
+ // a-trous and upsample kernels reach across the silhouette.
694
+ this.$outputs.outAO = pb.vec4(1);
695
+ }).$else(function() {
696
+ this.$l.worldNormal = pb.normalize(pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.uv, 0).rgb, 2), pb.vec3(1)));
446
697
  this.$l.viewNormal = pb.normalize(pb.mul(this.viewMatrix, pb.vec4(this.worldNormal, 0)).xyz);
447
- this.$l.numRays = pb.max(this.ssgiParams.w, 1);
448
- this.$l.sum = pb.vec3(0);
449
- this.$for(pb.float('ray'), 0, this.numRays, function() {
450
- this.$l.rand = SSGI_rand2(this, pb.vec2(this.$builtins.fragCoord.xy), pb.add(pb.mul(this.frameIndex, this.numRays), this.ray));
451
- this.$l.rayDir = SSGI_cosineSampleHemisphere(this, this.viewNormal, this.rand);
452
- this.$l.hitInfo = pb.vec4(0);
453
- if (ctx.HiZTexture) {
454
- this.hitInfo = screenSpaceRayTracing_HiZ(this, this.viewPos, this.rayDir, this.viewMatrix, this.projMatrix, this.invProjMatrix, this.cameraNearFar, this.depthMipLevels, this.ssgiParams.y, this.ssgiParams.x, this.ssgiParams.z, this.targetSize, this.hizTex, this.normalTex);
698
+ // Use the SH-integrated diffuse IBL as an exact, noise-free
699
+ // baseline. Stochastic rays only estimate the screen-space
700
+ // replacement relative to the environment in the same direction.
701
+ //
702
+ // getIrradiance returns the cosine-convolved SH, which already
703
+ // carries the 1/PI of the Lambert BRDF: materials consume it as
704
+ // k_D * irradiance with no further division, matching punctual
705
+ // lights that apply 1/PI explicitly. The published history is mixed
706
+ // against that same value in the lighting pass, so this baseline
707
+ // must stay in those units and must not be scaled by PI.
708
+ this.$l.iblIrradiance = pb.mul(ctx.env.light.envLight.getIrradiance(this, this.worldNormal), this.radianceParams.z);
709
+ this.$l.correctionSum = pb.vec3(0);
710
+ // Rays whose outcome the depth buffer cannot resolve (they left the
711
+ // screen or ran out of iterations behind geometry) are excluded from
712
+ // the average instead of counted as unoccluded sky.
713
+ this.$l.determinateSum = pb.float(0);
714
+ this.$l.escapedSum = pb.float(0);
715
+ // Ambient occlusion reuses the very same visibility measurement the
716
+ // sky removal is built on, so the two can never disagree. No extra
717
+ // ray march: only this accumulator is new.
718
+ this.$l.occludedSum = pb.float(0);
719
+ this.$for(pb.float('rayIndex'), 0, raysPerPixel, function() {
720
+ this.$l.xi = this.SSGI_hash22(pb.add(pb.mul(this.uv, this.targetSize.xy), pb.vec2(this.rayIndex, pb.mul(this.rayIndex, 7))), pb.add(this.radianceParams.w, pb.mul(this.rayIndex, 17)));
721
+ this.$l.worldRay = this.SSGI_cosineDirection(this.worldNormal, this.xi);
722
+ this.$l.viewRay = pb.normalize(pb.mul(this.viewMatrix, pb.vec4(this.worldRay, 0)).xyz);
723
+ this.$l.rayOrigin = pb.add(this.pos.xyz, pb.mul(this.viewNormal, this.traceParams.y, 0.25));
724
+ // vec3(occluded, escaped, rawConfidence) - see screenSpaceRayTracing_Linear2D.
725
+ this.$l.giTrace = pb.vec3(0);
726
+ if (useHiZ) {
727
+ // UE5 SSGI: diffuse rays march with RayRoughness = 1 (fast mip
728
+ // ramp) and StepOffset = noise - 0.9. A dedicated hash keeps
729
+ // the jitter decorrelated from the direction sample.
730
+ this.$l.stepOffset = pb.sub(this.SSGI_hash22(pb.add(pb.mul(this.uv, this.targetSize.xy), pb.vec2(pb.mul(this.rayIndex, 23), 11)), pb.add(this.radianceParams.w, pb.mul(this.rayIndex, 29))).x, 0.9);
731
+ this.$l.hit = screenSpaceRayTracing_HiZ(this, this.rayOrigin, this.viewRay, this.viewMatrix, this.projMatrix, this.invProjMatrix, this.cameraNearFar, this.depthMipLevels, this.traceParams.z, this.traceParams.x, this.traceParams.y, this.targetSize, this.hizTex, this.normalTex, this.giTrace, 1, this.stepOffset);
455
732
  } else {
456
- this.hitInfo = screenSpaceRayTracing_Linear2D(this, this.viewPos, this.rayDir, this.viewMatrix, this.projMatrix, this.invProjMatrix, this.cameraNearFar, this.ssgiParams.x, this.ssgiParams.y, this.ssgiParams.z, this.ssgiStride, this.targetSize, this.depthTex, this.normalTex, // Backface depth is only present when the depth prepass was
457
- // built with SSR thickness computation enabled.
458
- !!ctx.SSRCalcThickness);
733
+ this.$l.hit = screenSpaceRayTracing_Linear2D(this, this.rayOrigin, this.viewRay, this.viewMatrix, this.projMatrix, this.invProjMatrix, this.cameraNearFar, this.traceParams.x, this.traceParams.z, this.traceParams.y, this.traceParams.w, this.targetSize, this.depthTex, this.normalTex, false, this.giTrace);
459
734
  }
460
- this.$l.hitAlpha = pb.clamp(this.hitInfo.w, 0, 1);
461
- // Hemisphere rays frequently graze the screen plane; the
462
- // projective ray setup can then produce Inf/NaN which would be
463
- // smeared into large blocks by the bilateral blur. NaN fails
464
- // any comparison, so this test also rejects it.
465
- this.$l.hitValid = pb.and(pb.all(pb.lessThan(pb.abs(this.hitInfo), pb.vec4(1e30))), pb.greaterThan(this.hitAlpha, 0));
466
- this.$if(this.hitValid, function() {
467
- this.$l.hitUV = pb.clamp(this.hitInfo.xy, pb.vec2(0), pb.vec2(1));
468
- // Firefly clamp: an HDR sun/sky sample can be orders of
469
- // magnitude brighter than the surroundings; unclamped it
470
- // dominates the low-spp estimate as isolated bright dots no
471
- // spatial filter can remove.
472
- this.$l.radiance = pb.min(pb.textureSampleLevel(this.colorTex, this.hitUV, 0).rgb, pb.vec3(4));
473
- this.sum = pb.add(this.sum, pb.mul(this.radiance, this.hitAlpha));
474
- });
735
+ // Grazing/projectively degenerate rays can return Inf or NaN.
736
+ // A zero weight is not sufficient because NaN * 0 remains NaN and
737
+ // the spatial passes then spread it into a dark block.
738
+ this.$l.hitFinite = pb.all(pb.lessThan(pb.abs(this.hit), pb.vec4(1e30)));
739
+ // Visibility and radiance are kept apart: whether the ray was
740
+ // blocked is independent of whether the blocker's outgoing
741
+ // radiance can be read back from the screen. Geometric certainty
742
+ // gates occlusion so a thin-object false positive cannot darken,
743
+ // while the hit's screen colour is always a usable radiance
744
+ // estimate because the marcher only ever stops on-screen.
745
+ this.$l.occluded = pb.mul(pb.float(this.hitFinite), pb.clamp(this.giTrace.x, 0, 1), pb.clamp(this.giTrace.z, 0, 1));
746
+ this.$l.escaped = pb.mul(pb.float(this.hitFinite), pb.clamp(this.giTrace.y, 0, 1));
747
+ this.$l.hitUV = this.$choice(pb.greaterThan(this.occluded, 0), pb.clamp(this.hit.xy, pb.vec2(0), pb.vec2(1)), this.uv);
748
+ this.$l.rawEnvRadiance = pb.mul(ctx.env.light.envLight.getRadiance(this, this.worldRay, pb.float(0)), this.radianceParams.z);
749
+ this.$l.envRadiance = this.$choice(pb.all(pb.lessThan(pb.abs(this.rawEnvRadiance), pb.vec3(1e30))), this.rawEnvRadiance, pb.vec3(0));
750
+ this.$l.currentRadiance = pb.textureSampleLevel(this.currentColorTex, this.hitUV, 0).rgb;
751
+ if (sampleHistory) {
752
+ this.$l.hitMotion = pb.textureSampleLevel(this.motionTex, this.hitUV, 0).xy;
753
+ this.$l.previousHitUV = pb.sub(this.hitUV, this.hitMotion);
754
+ this.$l.historyValid = pb.and(pb.all(pb.greaterThanEqual(this.previousHitUV, pb.vec2(0))), pb.all(pb.lessThanEqual(this.previousHitUV, pb.vec2(1))), pb.all(pb.lessThan(pb.abs(this.hitMotion), pb.vec2(5e4))));
755
+ this.$l.historyRadiance = pb.textureSampleLevel(this.previousColorTex, pb.clamp(this.previousHitUV, pb.vec2(0), pb.vec2(1)), 0).rgb;
756
+ this.$l.currentHitDepth = pb.mul(ShaderHelper.sampleLinearDepth(this, this.depthTex, this.hitUV, 0), this.cameraNearFar.y);
757
+ this.$l.currentHitNormal = pb.normalize(pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.hitUV, 0).rgb, 2), pb.vec3(1)));
758
+ this.$l.previousHitSurface = pb.textureSampleLevel(this.previousSurfaceTex, pb.clamp(this.previousHitUV, pb.vec2(0), pb.vec2(1)), 0);
759
+ this.$l.previousHitNormal = pb.normalize(pb.sub(pb.mul(this.previousHitSurface.rgb, 2), pb.vec3(1)));
760
+ this.historyValid = pb.and(this.historyValid, pb.lessThanEqual(pb.abs(pb.sub(this.previousHitSurface.a, this.currentHitDepth)), this.historyRejectParams.x), pb.greaterThanEqual(pb.dot(this.currentHitNormal, this.previousHitNormal), this.historyRejectParams.y));
761
+ // Prefer the reprojected previous frame for multi-bounce
762
+ // feedback, but fall back to this frame's already-shaded opaque
763
+ // colour rather than dropping the sample. Losing one bounce of
764
+ // feedback is a far smaller error than reverting to unoccluded
765
+ // IBL, which is what made newly disoccluded regions flash.
766
+ this.$l.screenRadiance = this.$choice(this.historyValid, this.historyRadiance, this.currentRadiance);
767
+ } else {
768
+ this.$l.screenRadiance = this.currentRadiance;
769
+ }
770
+ this.$l.screenRadianceFinite = pb.all(pb.lessThan(pb.abs(this.screenRadiance), pb.vec3(1e30)));
771
+ this.screenRadiance = this.$choice(this.screenRadianceFinite, this.screenRadiance, this.envRadiance);
772
+ this.$l.maxComponent = pb.max(pb.max(this.screenRadiance.r, this.screenRadiance.g), this.screenRadiance.b);
773
+ this.$l.fireflyScale = this.$choice(pb.greaterThan(this.maxComponent, this.radianceParams.y), pb.div(this.radianceParams.y, pb.max(this.maxComponent, 1e-5)), pb.float(1));
774
+ this.$l.clampedScreenRadiance = pb.mul(this.screenRadiance, this.fireflyScale);
775
+ // Split the correction into the bounce the blocker adds and the sky
776
+ // it takes away. The bounce is always kept: the hit is on-screen, so
777
+ // its colour is a real measurement. Only the removal is scaled by
778
+ // skyOcclusion, which lets the sky be dimmed less than physically
779
+ // implied without also discarding measured bounce light.
780
+ this.$l.bounceGain = pb.max(pb.sub(this.clampedScreenRadiance, this.envRadiance), pb.vec3(0));
781
+ this.$l.skyLoss = pb.mul(pb.max(pb.sub(this.envRadiance, this.clampedScreenRadiance), pb.vec3(0)), this.skyOcclusion);
782
+ // Cosine-weighted sampling makes the estimator of E/PI - the unit
783
+ // the baseline and the lighting pass both use - the plain mean of
784
+ // the sampled radiance, so no PI appears here either.
785
+ this.$l.correction = pb.mul(pb.sub(this.bounceGain, this.skyLoss), this.occluded, this.radianceParams.x);
786
+ this.correctionSum = pb.add(this.correctionSum, this.correction);
787
+ this.determinateSum = pb.add(this.determinateSum, pb.max(this.occluded, this.escaped));
788
+ this.escapedSum = pb.add(this.escapedSum, this.escaped);
789
+ this.occludedSum = pb.add(this.occludedSum, this.occluded);
475
790
  });
476
- // Cosine-weighted sampling: the irradiance estimate over the
477
- // hemisphere reduces to the plain average of the hit radiances.
478
- this.color = pb.div(this.sum, this.numRays);
791
+ // Average over resolved rays only. Dividing by raysPerPixel would
792
+ // implicitly treat every indeterminate ray as an unoccluded sky
793
+ // sample, which is the systematic sky leak this pass has to avoid.
794
+ this.$l.determinateCount = pb.max(this.determinateSum, 1);
795
+ this.$l.irradiance = pb.add(this.iblIrradiance, pb.div(this.correctionSum, this.determinateCount));
796
+ // radianceParams.x blends the traced estimate over the IBL baseline. A finite-ray
797
+ // correction can have much higher variance than the integrated physical sky and must
798
+ // not remove more than that blend permits (at the default 0.7, at least 30% remains).
799
+ // That reserved share is itself scaled by measured visibility, otherwise no amount of
800
+ // occlusion could ever darken an enclosed corner.
801
+ this.$l.minimumIrradiance = pb.mul(this.iblIrradiance, pb.max(0, pb.sub(1, this.radianceParams.x)), pb.mix(pb.float(1), pb.div(this.escapedSum, this.determinateCount), this.skyOcclusion));
802
+ // Clamp before the render-target conversion: a finite value that overflows becomes Inf
803
+ // in the texture, and every a-trous pass then expands the contaminated region by its
804
+ // kernel radius. Pre-exposed lighting makes this unlikely, but a bright emissive hit
805
+ // amplified by a low-probability ray can still reach it.
806
+ this.$l.boundedIrradiance = pb.clamp(pb.max(this.irradiance, this.minimumIrradiance), pb.vec3(0), pb.vec3(HALF_FLOAT_MAX));
807
+ this.$outputs.outColor = pb.vec4(this.boundedIrradiance, 1);
808
+ // Cosine-weighted sampling makes the mean of `occluded` over the
809
+ // resolved rays an unbiased estimate of cosine-weighted visibility.
810
+ // Averaging over the determinate set - not raysPerPixel - is what
811
+ // keeps an indeterminate ray from reading as unoccluded, the same
812
+ // reasoning the irradiance average uses above.
813
+ this.$outputs.outAO = pb.vec4(pb.clamp(pb.sub(1, pb.div(this.occludedSum, this.determinateCount)), 0, 1));
479
814
  });
480
- // Reinhard-compress into [0, 1) (mirrors the SSR intersect pass).
481
- // The bilateral blur weights neighbors by color distance; in linear
482
- // HDR a firefly is "far" from every neighbor and survives the blur
483
- // untouched. Compressed, bright outliers stay within blurring range
484
- // and the denoiser can actually converge them. combine() inverts
485
- // this mapping.
486
- this.color = pb.div(this.color, pb.add(this.color, pb.vec3(1)));
487
- this.$outputs.outColor = pb.vec4(this.color, 1);
488
815
  });
489
816
  }
490
817
  });
491
- if (!program) {
492
- return null;
493
- }
494
818
  program.name = '@SSGI_Trace';
495
819
  return program;
496
820
  }
497
- /** @internal */ _createTemporalProgram(ctx) {
821
+ /** @internal */ createTemporalProgram(ctx, hasHistory) {
498
822
  const program = ctx.device.buildRenderProgram({
499
823
  vertex (pb) {
500
824
  this.flip = pb.int().uniform(0);
501
825
  this.$inputs.pos = pb.vec2().attrib('position');
502
826
  this.$outputs.uv = pb.vec2();
503
827
  pb.main(function() {
504
- this.$builtins.position = pb.vec4(this.$inputs.pos, 1, 1);
828
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
505
829
  this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
506
830
  this.$if(pb.notEqual(this.flip, 0), function() {
507
831
  this.$builtins.position.y = pb.neg(this.$builtins.position.y);
@@ -509,95 +833,279 @@ import { FrameResources } from '../render/rendergraph/blackboard.js';
509
833
  });
510
834
  },
511
835
  fragment (pb) {
512
- this.historyColorTex = pb.tex2D().uniform(0);
513
- this.currentColorTex = pb.tex2D().uniform(0);
514
- this.motionVector = pb.tex2D().uniform(0);
515
- this.prevMotionVector = pb.tex2D().uniform(0);
516
- this.texSize = pb.vec2().uniform(0);
517
- this.temporalWeight = pb.float().uniform(0);
518
- this.$outputs.outColor = pb.vec4();
836
+ this.currentTex = pb.tex2D().uniform(0);
837
+ this.currentAOTex = pb.tex2D().uniform(0);
838
+ this.depthTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
839
+ this.normalTex = pb.tex2D().uniform(0);
840
+ if (hasHistory) {
841
+ this.motionTex = pb.tex2D().uniform(0);
842
+ this.previousIrradianceTex = pb.tex2D().uniform(0);
843
+ this.previousSurfaceTex = pb.tex2D().uniform(0);
844
+ this.previousMomentsTex = pb.tex2D().uniform(0);
845
+ this.previousAOTex = pb.tex2D().uniform(0);
846
+ }
847
+ this.targetSize = pb.vec4().uniform(0);
848
+ this.temporalParams = pb.vec4().uniform(0);
849
+ this.$outputs.outIrradiance = pb.vec4();
850
+ this.$outputs.outMoments = pb.vec4();
851
+ this.$outputs.outAO = pb.vec4();
852
+ pb.func('SSGI_luminance', [
853
+ pb.vec3('c')
854
+ ], function() {
855
+ this.$l.boundedLuminance = pb.clamp(pb.dot(this.c, pb.vec3(0.2126, 0.7152, 0.0722)), 0, MOMENT_LUMINANCE_MAX);
856
+ this.$return(this.boundedLuminance);
857
+ });
519
858
  pb.main(function() {
520
- // Sample-count adaptive accumulation with fixed-gamma variance
521
- // clipping. The accumulated frame count N rides in the alpha
522
- // channel of the radiance history: early frames blend with
523
- // 1/(N+1) (fast convergence), converged pixels blend at the
524
- // temporalWeight cap. A FIXED-weight EMA never settles - its
525
- // stationary state is a random walk inside the clip box, visible
526
- // as slowly rippling low-frequency noise after the blur. With
527
- // count-based weighting the estimator is a true running average,
528
- // so the converged value stops moving.
529
- // TAA-style box collapsing with velocity is deliberately avoided:
530
- // camera jitter alone would collapse the box and reject all
531
- // history, preventing convergence of the stochastic signal.
532
- this.$l.screenUV = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.texSize);
533
- this.$l.currentColor = pb.textureSampleLevel(this.currentColorTex, this.screenUV, 0).rgb;
534
- this.$l.velocitySample = pb.textureSampleLevel(this.motionVector, this.screenUV, 0);
535
- this.$l.velocity = this.velocitySample.xy;
536
- // Sky/unwritten motion vector sentinel: no history
537
- this.$if(pb.and(pb.greaterThanEqual(this.velocity.x, 5e4), pb.greaterThanEqual(this.velocity.y, 5e4)), function() {
538
- this.$outputs.outColor = pb.vec4(this.currentColor, 1);
539
- }).$else(function() {
540
- this.$l.reprojectedUV = pb.sub(this.screenUV, this.velocity);
541
- this.$l.offscreen = pb.or(pb.any(pb.lessThan(this.reprojectedUV, pb.vec2(0))), pb.any(pb.greaterThan(this.reprojectedUV, pb.vec2(1))));
542
- this.$if(this.offscreen, function() {
543
- this.$outputs.outColor = pb.vec4(this.currentColor, 1);
544
- }).$else(function() {
545
- this.$l.historySample = pb.textureSampleLevel(this.historyColorTex, this.reprojectedUV, 0);
546
- this.$l.historyColor = this.historySample.rgb;
547
- this.$l.historyCount = pb.max(this.historySample.a, 1);
548
- // 3x3 neighborhood statistics of the current stochastic estimate
549
- this.$l.m1 = pb.vec3(0);
550
- this.$l.m2 = pb.vec3(0);
551
- for(let i = -1; i <= 1; i++){
552
- for(let j = -1; j <= 1; j++){
553
- const c = `c${(i + 1) * 3 + j + 1}`;
554
- this.$l[c] = pb.textureSampleLevel(this.currentColorTex, pb.add(this.screenUV, pb.div(pb.vec2(i, j), this.texSize)), 0).rgb;
555
- this.m1 = pb.add(this.m1, this[c]);
556
- this.m2 = pb.add(this.m2, pb.mul(this[c], this[c]));
859
+ this.$l.uv = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize.xy);
860
+ this.$l.current = pb.textureSampleLevel(this.currentTex, this.uv, 0);
861
+ this.$l.currentAO = pb.textureSampleLevel(this.currentAOTex, this.uv, 0).r;
862
+ this.$l.result = this.current.rgb;
863
+ this.$l.resultAO = this.currentAO;
864
+ this.$l.luminance = this.SSGI_luminance(this.current.rgb);
865
+ this.$l.moment = pb.vec4(this.luminance, pb.mul(this.luminance, this.luminance), 1, 0);
866
+ if (hasHistory) {
867
+ this.$l.motion = pb.textureSampleLevel(this.motionTex, this.uv, 0).xy;
868
+ this.$l.previousUV = pb.sub(this.uv, this.motion);
869
+ this.$l.validUV = pb.and(pb.all(pb.greaterThanEqual(this.previousUV, pb.vec2(0))), pb.all(pb.lessThanEqual(this.previousUV, pb.vec2(1))), pb.all(pb.lessThan(pb.abs(this.motion), pb.vec2(5e4))));
870
+ this.$l.currentDepth = pb.mul(ShaderHelper.sampleLinearDepth(this, this.depthTex, this.uv, 0), this.temporalParams.w);
871
+ this.$l.currentNormal = pb.normalize(pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.uv, 0).rgb, 2), pb.vec3(1)));
872
+ this.$l.previousSurface = pb.textureSampleLevel(this.previousSurfaceTex, pb.clamp(this.previousUV, pb.vec2(0), pb.vec2(1)), 0);
873
+ this.$l.previousNormal = pb.normalize(pb.sub(pb.mul(this.previousSurface.rgb, 2), pb.vec3(1)));
874
+ this.$l.surfaceValid = pb.and(pb.lessThanEqual(pb.abs(pb.sub(this.previousSurface.a, this.currentDepth)), this.temporalParams.y), pb.greaterThanEqual(pb.dot(this.currentNormal, this.previousNormal), this.temporalParams.z));
875
+ this.$l.neighborhoodMin = this.current.rgb;
876
+ this.$l.neighborhoodMax = this.current.rgb;
877
+ this.$l.aoNeighborhoodMin = this.currentAO;
878
+ this.$l.aoNeighborhoodMax = this.currentAO;
879
+ for(let y = -1; y <= 1; y++){
880
+ for(let x = -1; x <= 1; x++){
881
+ if (x === 0 && y === 0) {
882
+ continue;
557
883
  }
884
+ this.$l.neighborUV = pb.add(this.uv, pb.div(pb.vec2(x, y), this.targetSize.xy));
885
+ this.$l.neighbor = pb.textureSampleLevel(this.currentTex, this.neighborUV, 0).rgb;
886
+ this.neighborhoodMin = pb.min(this.neighborhoodMin, this.neighbor);
887
+ this.neighborhoodMax = pb.max(this.neighborhoodMax, this.neighbor);
888
+ this.$l.aoNeighbor = pb.textureSampleLevel(this.currentAOTex, this.neighborUV, 0).r;
889
+ this.aoNeighborhoodMin = pb.min(this.aoNeighborhoodMin, this.aoNeighbor);
890
+ this.aoNeighborhoodMax = pb.max(this.aoNeighborhoodMax, this.aoNeighbor);
558
891
  }
559
- this.m1 = pb.div(this.m1, 9);
560
- this.m2 = pb.div(this.m2, 9);
561
- this.$l.sigma = pb.sqrt(pb.max(pb.sub(this.m2, pb.mul(this.m1, this.m1)), pb.vec3(0)));
562
- // gamma = 2 with a small absolute slack: tolerant to the noise a
563
- // converged history legitimately sits in (compressed-space values).
564
- this.$l.halfExtent = pb.add(pb.mul(this.sigma, 2), pb.vec3(0.03));
565
- this.$l.clampedHistory = pb.clamp(this.historyColor, pb.sub(this.m1, this.halfExtent), pb.add(this.m1, this.halfExtent));
566
- // How much the clip moved the history signals stale data;
567
- // knock the effective count down so recovery is fast.
568
- this.$l.clipAmount = pb.length(pb.sub(this.clampedHistory, this.historyColor));
569
- this.$l.clipReset = pb.clamp(pb.mul(this.clipAmount, 8), 0, 1);
570
- // Disocclusion: previous frame's motion at the reprojected point
571
- // should agree with the current motion; a large mismatch means the
572
- // surface was not visible last frame.
573
- this.$l.prevVelocity = pb.textureSampleLevel(this.prevMotionVector, this.reprojectedUV, 0).xy;
574
- this.$l.velocityDiff = pb.length(pb.mul(pb.sub(this.velocity, this.prevVelocity), this.texSize));
575
- this.$l.disocclusion = pb.clamp(pb.mul(pb.sub(this.velocityDiff, 2.5), 0.1), 0, 1);
576
- this.$l.confidence = pb.mul(pb.sub(1, this.disocclusion), pb.sub(1, this.clipReset));
577
- this.$l.effectiveCount = pb.max(pb.mul(this.historyCount, this.confidence), 0);
578
- // Count-based blend weight, capped by temporalWeight:
579
- // maxCount = w/(1-w) so the cap and the weight limit agree.
580
- this.$l.wMax = pb.clamp(this.temporalWeight, 0, 0.99);
581
- this.$l.maxCount = pb.div(this.wMax, pb.sub(1, this.wMax));
582
- this.$l.w = pb.min(this.wMax, pb.div(this.effectiveCount, pb.add(this.effectiveCount, 1)));
583
- this.$l.newCount = pb.min(pb.add(this.effectiveCount, 1), this.maxCount);
584
- this.$outputs.outColor = pb.vec4(pb.mix(this.currentColor, this.clampedHistory, this.w), this.newCount);
585
- });
892
+ }
893
+ this.$l.previous = pb.textureSampleLevel(this.previousIrradianceTex, pb.clamp(this.previousUV, pb.vec2(0), pb.vec2(1)), 0);
894
+ this.$l.previousClamped = pb.clamp(this.previous.rgb, this.neighborhoodMin, this.neighborhoodMax);
895
+ this.$l.previousMoment = pb.textureSampleLevel(this.previousMomentsTex, pb.clamp(this.previousUV, pb.vec2(0), pb.vec2(1)), 0);
896
+ this.$l.validity = pb.mul(pb.float(pb.and(this.validUV, this.surfaceValid)), pb.clamp(this.previous.a, 0, 1));
897
+ // Start new histories as a running average, then cap their weight
898
+ // at the configured value once enough samples have accumulated.
899
+ this.$l.runningAverageWeight = pb.div(this.previousMoment.z, pb.add(this.previousMoment.z, 1));
900
+ this.$l.historyWeight = pb.min(this.temporalParams.x, this.runningAverageWeight);
901
+ this.$l.weight = pb.mul(this.historyWeight, this.validity);
902
+ this.result = pb.mix(this.current.rgb, this.previousClamped, this.weight);
903
+ // AO accumulates on exactly the same validity and weight as the
904
+ // irradiance: it was measured by the same rays against the same
905
+ // surface, so any disagreement between the two would be an artifact.
906
+ this.$l.previousAO = pb.textureSampleLevel(this.previousAOTex, pb.clamp(this.previousUV, pb.vec2(0), pb.vec2(1)), 0).r;
907
+ this.$l.previousAOClamped = pb.clamp(this.previousAO, this.aoNeighborhoodMin, this.aoNeighborhoodMax);
908
+ this.resultAO = pb.mix(this.currentAO, this.previousAOClamped, this.weight);
909
+ this.$l.momentXY = pb.mix(this.moment.xy, this.previousMoment.xy, this.weight);
910
+ this.$l.historyLength = pb.mix(pb.float(1), pb.min(32, pb.add(this.previousMoment.z, 1)), this.validity);
911
+ this.$l.variance = pb.max(0, pb.sub(this.momentXY.y, pb.mul(this.momentXY.x, this.momentXY.x)));
912
+ this.moment = pb.vec4(this.momentXY, this.historyLength, this.variance);
913
+ } else {
914
+ // The WebGL fallback has no motion-vector temporal resolve. Seed
915
+ // a-trous with spatial variance so its luminance weighting can
916
+ // still remove Monte Carlo noise instead of treating it as an edge.
917
+ this.$l.spatialLum = pb.float(0);
918
+ this.$l.spatialLum2 = pb.float(0);
919
+ for(let y = -1; y <= 1; y++){
920
+ for(let x = -1; x <= 1; x++){
921
+ this.$l.neighborLum = this.SSGI_luminance(pb.textureSampleLevel(this.currentTex, pb.add(this.uv, pb.div(pb.vec2(x, y), this.targetSize.xy)), 0).rgb);
922
+ this.spatialLum = pb.add(this.spatialLum, this.neighborLum);
923
+ this.spatialLum2 = pb.add(this.spatialLum2, pb.mul(this.neighborLum, this.neighborLum));
924
+ }
925
+ }
926
+ this.spatialLum = pb.div(this.spatialLum, 9);
927
+ this.spatialLum2 = pb.div(this.spatialLum2, 9);
928
+ this.$l.spatialVariance = pb.max(0, pb.sub(this.spatialLum2, pb.mul(this.spatialLum, this.spatialLum)));
929
+ this.moment = pb.vec4(this.moment.xyz, this.spatialVariance);
930
+ }
931
+ this.$outputs.outIrradiance = pb.vec4(this.result, this.current.a);
932
+ this.$outputs.outMoments = this.moment;
933
+ this.$outputs.outAO = pb.vec4(pb.clamp(this.resultAO, 0, 1));
934
+ });
935
+ }
936
+ });
937
+ program.name = hasHistory ? '@SSGI_Temporal' : '@SSGI_TemporalInitialize';
938
+ return program;
939
+ }
940
+ /** @internal */ createAtrousProgram(ctx) {
941
+ const program = ctx.device.buildRenderProgram({
942
+ vertex (pb) {
943
+ this.flip = pb.int().uniform(0);
944
+ this.$inputs.pos = pb.vec2().attrib('position');
945
+ this.$outputs.uv = pb.vec2();
946
+ pb.main(function() {
947
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
948
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
949
+ this.$if(pb.notEqual(this.flip, 0), function() {
950
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
951
+ });
952
+ });
953
+ },
954
+ fragment (pb) {
955
+ this.sourceTex = pb.tex2D().uniform(0);
956
+ this.aoSourceTex = pb.tex2D().uniform(0);
957
+ this.momentsTex = pb.tex2D().uniform(0);
958
+ this.depthTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
959
+ this.normalTex = pb.tex2D().uniform(0);
960
+ this.targetSize = pb.vec4().uniform(0);
961
+ this.filterParams = pb.vec4().uniform(0);
962
+ this.denoiseParams = pb.vec4().uniform(0);
963
+ this.cameraFar = pb.float().uniform(0);
964
+ this.$outputs.outColor = pb.vec4();
965
+ this.$outputs.outAO = pb.vec4();
966
+ pb.func('SSGI_luminance', [
967
+ pb.vec3('c')
968
+ ], function() {
969
+ this.$return(pb.max(0, pb.dot(this.c, pb.vec3(0.2126, 0.7152, 0.0722))));
970
+ });
971
+ pb.main(function() {
972
+ this.$l.uv = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize.xy);
973
+ this.$l.center = pb.textureSampleLevel(this.sourceTex, this.uv, 0);
974
+ this.$l.centerDepth = pb.mul(ShaderHelper.sampleLinearDepth(this, this.depthTex, this.uv, 0), this.cameraFar);
975
+ this.$l.centerNormal = pb.normalize(pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.uv, 0).rgb, 2), pb.vec3(1)));
976
+ this.$l.centerLum = this.SSGI_luminance(this.center.rgb);
977
+ this.$l.moments = pb.textureSampleLevel(this.momentsTex, this.uv, 0);
978
+ // A pixel with little accumulated history has an unreliable variance
979
+ // estimate, so the edge-stopping functions would preserve Monte Carlo
980
+ // noise as if it were detail. Widen the kernel and relax the luminance
981
+ // and normal tests until enough frames have converged, then hand back
982
+ // to the sharp weights (standard SVGF history-driven ramp).
983
+ //
984
+ // A path that never accumulates history (no temporal resolve, so
985
+ // moments.z is pinned at 1) is permanently unconverged, not converged:
986
+ // it is the noisiest input this filter ever sees. Sending it to the
987
+ // relaxed end is the whole point of the ramp - the sharp end would give
988
+ // it the narrowest kernel and one eighth the luminance tolerance.
989
+ this.$l.historyConfidence = this.$choice(pb.greaterThan(this.denoiseParams.x, 0), pb.clamp(pb.div(this.moments.z, this.denoiseParams.x), 0, 1), pb.float(0));
990
+ this.$l.stepScale = pb.mix(pb.float(2), pb.float(1), this.historyConfidence);
991
+ this.$l.effStep = pb.mul(this.filterParams.x, this.stepScale);
992
+ this.$l.normalPower = pb.mix(pb.max(1, pb.mul(this.filterParams.z, 0.25)), this.filterParams.z, this.historyConfidence);
993
+ this.$l.sigmaScale = pb.mix(pb.float(8), pb.float(1), this.historyConfidence);
994
+ this.$l.sigma = pb.mul(pb.add(pb.sqrt(pb.max(this.moments.w, 0)), 0.02), this.sigmaScale);
995
+ this.$l.centerAO = pb.textureSampleLevel(this.aoSourceTex, this.uv, 0).r;
996
+ this.$l.sum = pb.vec3(0);
997
+ this.$l.weightSum = pb.float(0);
998
+ this.$l.aoSum = pb.float(0);
999
+ this.$l.aoWeightSum = pb.float(0);
1000
+ const kernel = [
1001
+ 1,
1002
+ 2 / 3,
1003
+ 1 / 6
1004
+ ];
1005
+ for(let y = -2; y <= 2; y++){
1006
+ for(let x = -2; x <= 2; x++){
1007
+ const kernelWeight = kernel[Math.abs(x)] * kernel[Math.abs(y)];
1008
+ this.$l.sampleUV = pb.add(this.uv, pb.mul(pb.div(pb.vec2(x, y), this.targetSize.xy), this.effStep));
1009
+ this.$l.sampleValue = pb.textureSampleLevel(this.sourceTex, this.sampleUV, 0);
1010
+ this.$l.sampleDepth = pb.mul(ShaderHelper.sampleLinearDepth(this, this.depthTex, this.sampleUV, 0), this.cameraFar);
1011
+ this.$l.sampleNormal = pb.normalize(pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.sampleUV, 0).rgb, 2), pb.vec3(1)));
1012
+ this.$l.depthWeight = pb.exp(pb.neg(pb.div(pb.abs(pb.sub(this.sampleDepth, this.centerDepth)), pb.mul(this.filterParams.y, this.effStep))));
1013
+ this.$l.normalWeight = pb.pow(pb.max(0, pb.dot(this.centerNormal, this.sampleNormal)), this.normalPower);
1014
+ this.$l.colorWeight = pb.exp(pb.neg(pb.div(pb.abs(pb.sub(this.SSGI_luminance(this.sampleValue.rgb), this.centerLum)), pb.mul(this.sigma, this.filterParams.w))));
1015
+ this.$l.weight = pb.mul(kernelWeight, this.depthWeight, this.normalWeight, this.colorWeight, this.sampleValue.a);
1016
+ this.sum = pb.add(this.sum, pb.mul(this.sampleValue.rgb, this.weight));
1017
+ this.weightSum = pb.add(this.weightSum, this.weight);
1018
+ // AO deliberately drops colorWeight: that term is driven by the
1019
+ // moments' luminance variance, which describes the irradiance and
1020
+ // says nothing about AO. Leaving it out is a stronger smoothing,
1021
+ // which is what a per-ray binary quantity needs - with 1-2 rays per
1022
+ // pixel, single-frame AO is close to binary.
1023
+ this.$l.aoWeight = pb.mul(kernelWeight, this.depthWeight, this.normalWeight, this.sampleValue.a);
1024
+ this.$l.sampleAO = pb.textureSampleLevel(this.aoSourceTex, this.sampleUV, 0).r;
1025
+ this.aoSum = pb.add(this.aoSum, pb.mul(this.sampleAO, this.aoWeight));
1026
+ this.aoWeightSum = pb.add(this.aoWeightSum, this.aoWeight);
1027
+ }
1028
+ }
1029
+ this.$l.filtered = this.$choice(pb.greaterThan(this.weightSum, 1e-5), pb.div(this.sum, this.weightSum), this.center.rgb);
1030
+ this.$l.filteredAO = this.$choice(pb.greaterThan(this.aoWeightSum, 1e-5), pb.div(this.aoSum, this.aoWeightSum), this.centerAO);
1031
+ this.$outputs.outColor = pb.vec4(this.filtered, this.center.a);
1032
+ this.$outputs.outAO = pb.vec4(pb.clamp(this.filteredAO, 0, 1));
1033
+ });
1034
+ }
1035
+ });
1036
+ program.name = '@SSGI_ATrous';
1037
+ return program;
1038
+ }
1039
+ /** @internal */ createSurfaceProgram(ctx) {
1040
+ const program = ctx.device.buildRenderProgram({
1041
+ vertex (pb) {
1042
+ this.flip = pb.int().uniform(0);
1043
+ this.$inputs.pos = pb.vec2().attrib('position');
1044
+ this.$outputs.uv = pb.vec2();
1045
+ pb.main(function() {
1046
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
1047
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
1048
+ this.$if(pb.notEqual(this.flip, 0), function() {
1049
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
586
1050
  });
587
1051
  });
1052
+ },
1053
+ fragment (pb) {
1054
+ this.depthTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
1055
+ this.normalTex = pb.tex2D().uniform(0);
1056
+ this.targetSize = pb.vec2().uniform(0);
1057
+ this.cameraFar = pb.float().uniform(0);
1058
+ this.$outputs.outColor = pb.vec4();
1059
+ pb.main(function() {
1060
+ this.$l.uv = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize);
1061
+ this.$l.normal = pb.textureSampleLevel(this.normalTex, this.uv, 0).rgb;
1062
+ this.$l.depth = pb.mul(ShaderHelper.sampleLinearDepth(this, this.depthTex, this.uv, 0), this.cameraFar);
1063
+ this.$outputs.outColor = pb.vec4(this.normal, this.depth);
1064
+ });
588
1065
  }
589
1066
  });
590
- program.name = '@SSGI_Temporal';
1067
+ program.name = '@SSGI_Surface';
591
1068
  return program;
592
1069
  }
593
- /** @internal */ _createCombineProgram(ctx) {
1070
+ /**
1071
+ * Multiply the traced ambient occlusion into the scene color.
1072
+ *
1073
+ * This is the same multiply-into-final-color semantics the standalone SAO post
1074
+ * effect applies, so enabling both double darkens.
1075
+ *
1076
+ * @internal
1077
+ */ compositeAO(ctx, sceneColor, ao, srgbOutput) {
1078
+ const key = srgbOutput ? 'srgb' : 'linear';
1079
+ let program = SSGI._compositePrograms[key];
1080
+ if (!program) {
1081
+ program = this.createCompositeProgram(ctx, srgbOutput);
1082
+ SSGI._compositePrograms[key] = program;
1083
+ }
1084
+ const hash = `${key}:${sceneColor.uid}:${ao.uid}`;
1085
+ let bindGroup = this._compositeBindGroups[hash];
1086
+ if (!bindGroup) {
1087
+ bindGroup = ctx.device.createBindGroup(program.bindGroupLayouts[0]);
1088
+ bindGroup.setTexture('colorTex', sceneColor, fetchSampler('clamp_nearest_nomip'));
1089
+ // Nearest, not linear: the AO handed to this pass is already at full
1090
+ // resolution, so filtering would only add a dependency on the WebGL
1091
+ // half-float linear filtering cap for no gain.
1092
+ bindGroup.setTexture('aoTex', ao, fetchSampler('clamp_nearest_nomip'));
1093
+ this._compositeBindGroups[hash] = bindGroup;
1094
+ }
1095
+ bindGroup.setValue('aoParams', new Vector2(ctx.camera.ssgiAOIntensity, Math.max(1e-3, ctx.camera.ssgiAOPower)));
1096
+ bindGroup.setValue('flip', this.needFlip(ctx.device) ? 1 : 0);
1097
+ ctx.device.setProgram(program);
1098
+ ctx.device.setBindGroup(0, bindGroup);
1099
+ this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
1100
+ }
1101
+ /** @internal */ createCompositeProgram(ctx, srgbOutput) {
594
1102
  const program = ctx.device.buildRenderProgram({
595
1103
  vertex (pb) {
596
1104
  this.flip = pb.int().uniform(0);
597
1105
  this.$inputs.pos = pb.vec2().attrib('position');
598
1106
  this.$outputs.uv = pb.vec2();
599
1107
  pb.main(function() {
600
- this.$builtins.position = pb.vec4(this.$inputs.pos, 1, 1);
1108
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
601
1109
  this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
602
1110
  this.$if(pb.notEqual(this.flip, 0), function() {
603
1111
  this.$builtins.position.y = pb.neg(this.$builtins.position.y);
@@ -606,36 +1114,80 @@ import { FrameResources } from '../render/rendergraph/blackboard.js';
606
1114
  },
607
1115
  fragment (pb) {
608
1116
  this.colorTex = pb.tex2D().uniform(0);
609
- this.radianceTex = pb.tex2D().uniform(0);
610
- this.targetSize = pb.vec2().uniform(0);
611
- this.ssgiIntensity = pb.float().uniform(0);
612
- this.srgbOut = pb.int().uniform(0);
1117
+ this.aoTex = pb.tex2D().uniform(0);
1118
+ this.aoParams = pb.vec2().uniform(0);
613
1119
  this.$outputs.outColor = pb.vec4();
614
1120
  pb.main(function() {
615
- this.$l.screenUV = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize);
616
- this.$l.sceneColor = pb.textureSampleLevel(this.colorTex, this.screenUV, 0).rgb;
617
- // Invert the Reinhard compression applied by the trace pass
618
- // (x/(1+x) -> y/(1-y)); the clamp bounds the expansion so a fully
619
- // saturated texel cannot blow up to infinity.
620
- this.$l.compressed = pb.clamp(pb.textureSampleLevel(this.radianceTex, this.screenUV, 0).rgb, pb.vec3(0), pb.vec3(0.98));
621
- this.$l.radiance = pb.div(this.compressed, pb.sub(pb.vec3(1), this.compressed));
622
- // The forward pipeline keeps no albedo GBuffer; approximate the
623
- // surface reflectance by the chromaticity of the lit scene color
624
- // (hue preserved, luminance normalized). Unlike scaling by the
625
- // scene color itself this keeps indirect light visible in shadowed
626
- // areas - exactly where GI matters.
627
- this.$l.luma = pb.dot(this.sceneColor, pb.vec3(0.2126, 0.7152, 0.0722));
628
- this.$l.albedoApprox = pb.clamp(pb.div(this.sceneColor, pb.vec3(pb.add(this.luma, 1e-3))), pb.vec3(0), pb.vec3(1));
629
- this.$l.combined = pb.add(this.sceneColor, pb.mul(this.radiance, this.albedoApprox, this.ssgiIntensity));
630
- this.$if(pb.equal(this.srgbOut, 0), function() {
631
- this.$outputs.outColor = pb.vec4(this.combined, 1);
632
- }).$else(function() {
633
- this.$outputs.outColor = pb.vec4(linearToGamma(this, this.combined), 1);
1121
+ this.$l.color = pb.textureSampleLevel(this.colorTex, this.$inputs.uv, 0);
1122
+ this.$l.ao = pb.clamp(pb.textureSampleLevel(this.aoTex, this.$inputs.uv, 0).r, 0, 1);
1123
+ // mix() toward 1 rather than a plain multiply: intensity then doubles as
1124
+ // the fallback for noisy or unresolved texels, which matters most on the
1125
+ // WebGL path where there is no temporal accumulation to converge them.
1126
+ this.$l.shapedAO = pb.pow(this.ao, this.aoParams.y);
1127
+ this.$l.finalAO = pb.mix(pb.float(1), this.shapedAO, this.aoParams.x);
1128
+ this.$l.occludedColor = pb.mul(this.color.rgb, this.finalAO);
1129
+ this.$outputs.outColor = pb.vec4(srgbOutput ? linearToGamma(this, this.occludedColor) : this.occludedColor, this.color.a);
1130
+ });
1131
+ }
1132
+ });
1133
+ program.name = srgbOutput ? '@SSGI_CompositeAO_sRGB' : '@SSGI_CompositeAO';
1134
+ return program;
1135
+ }
1136
+ /** @internal */ createUpsampleProgram(ctx) {
1137
+ const program = ctx.device.buildRenderProgram({
1138
+ vertex (pb) {
1139
+ this.flip = pb.int().uniform(0);
1140
+ this.$inputs.pos = pb.vec2().attrib('position');
1141
+ this.$outputs.uv = pb.vec2();
1142
+ pb.main(function() {
1143
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
1144
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
1145
+ this.$if(pb.notEqual(this.flip, 0), function() {
1146
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
634
1147
  });
635
1148
  });
1149
+ },
1150
+ fragment (pb) {
1151
+ this.sourceTex = pb.tex2D().uniform(0);
1152
+ this.aoSourceTex = pb.tex2D().uniform(0);
1153
+ this.depthTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
1154
+ this.normalTex = pb.tex2D().uniform(0);
1155
+ this.targetSize = pb.vec4().uniform(0);
1156
+ this.guideParams = pb.vec4().uniform(0);
1157
+ this.$outputs.outColor = pb.vec4();
1158
+ this.$outputs.outAO = pb.vec4();
1159
+ pb.main(function() {
1160
+ this.$l.uv = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize.zw);
1161
+ this.$l.centerDepth = pb.mul(ShaderHelper.sampleLinearDepth(this, this.depthTex, this.uv, 0), this.guideParams.z);
1162
+ this.$l.centerNormal = pb.normalize(pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.uv, 0).rgb, 2), pb.vec3(1)));
1163
+ this.$l.sum = pb.vec3(0);
1164
+ this.$l.weightSum = pb.float(0);
1165
+ this.$l.aoSum = pb.float(0);
1166
+ for(let y = -1; y <= 1; y++){
1167
+ for(let x = -1; x <= 1; x++){
1168
+ this.$l.sampleUV = pb.add(this.uv, pb.div(pb.vec2(x, y), this.targetSize.xy));
1169
+ this.$l.sampleValue = pb.textureSampleLevel(this.sourceTex, this.sampleUV, 0);
1170
+ this.$l.sampleAO = pb.textureSampleLevel(this.aoSourceTex, this.sampleUV, 0).r;
1171
+ this.$l.sampleDepth = pb.mul(ShaderHelper.sampleLinearDepth(this, this.depthTex, this.sampleUV, 0), this.guideParams.z);
1172
+ this.$l.sampleNormal = pb.normalize(pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.sampleUV, 0).rgb, 2), pb.vec3(1)));
1173
+ this.$l.depthWeight = pb.exp(pb.neg(pb.div(pb.abs(pb.sub(this.sampleDepth, this.centerDepth)), this.guideParams.x)));
1174
+ this.$l.normalWeight = pb.pow(pb.max(0, pb.dot(this.centerNormal, this.sampleNormal)), this.guideParams.y);
1175
+ this.$l.weight = pb.mul(this.depthWeight, this.normalWeight, this.sampleValue.a);
1176
+ this.sum = pb.add(this.sum, pb.mul(this.sampleValue.rgb, this.weight));
1177
+ this.weightSum = pb.add(this.weightSum, this.weight);
1178
+ // Same joint bilateral weights as the irradiance, so the upsampled
1179
+ // AO cannot cross a depth or normal edge the irradiance respects.
1180
+ this.aoSum = pb.add(this.aoSum, pb.mul(this.sampleAO, this.weight));
1181
+ }
1182
+ }
1183
+ this.$l.result = this.$choice(pb.greaterThan(this.weightSum, 1e-5), pb.div(this.sum, this.weightSum), pb.textureSampleLevel(this.sourceTex, this.uv, 0).rgb);
1184
+ this.$l.resultAO = this.$choice(pb.greaterThan(this.weightSum, 1e-5), pb.div(this.aoSum, this.weightSum), pb.textureSampleLevel(this.aoSourceTex, this.uv, 0).r);
1185
+ this.$outputs.outColor = pb.vec4(this.result, pb.float(pb.greaterThan(this.weightSum, 1e-5)));
1186
+ this.$outputs.outAO = pb.vec4(pb.clamp(this.resultAO, 0, 1));
1187
+ });
636
1188
  }
637
1189
  });
638
- program.name = '@SSGI_Combine';
1190
+ program.name = '@SSGI_Upsample';
639
1191
  return program;
640
1192
  }
641
1193
  }