@zephyr3d/scene 0.9.21 → 0.9.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/animation/joint_dynamics/collision.js +1 -7
  2. package/dist/animation/joint_dynamics/collision.js.map +1 -1
  3. package/dist/animation/joint_dynamics/constraints.js +2 -17
  4. package/dist/animation/joint_dynamics/constraints.js.map +1 -1
  5. package/dist/animation/joint_dynamics/controller.js +2 -20
  6. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  7. package/dist/animation/joint_dynamics/joint_dynamics_system.js +1 -3
  8. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  9. package/dist/animation/joint_dynamics/solver.js +14 -60
  10. package/dist/animation/joint_dynamics/solver.js.map +1 -1
  11. package/dist/animation/joint_dynamics/types.js +3 -3
  12. package/dist/animation/joint_dynamics/types.js.map +1 -1
  13. package/dist/app/app.js +23 -0
  14. package/dist/app/app.js.map +1 -1
  15. package/dist/app/scriptregistry.js +16 -53
  16. package/dist/app/scriptregistry.js.map +1 -1
  17. package/dist/asset/assetmanager.js +2 -0
  18. package/dist/asset/assetmanager.js.map +1 -1
  19. package/dist/asset/model.js +22 -0
  20. package/dist/asset/model.js.map +1 -1
  21. package/dist/blitter/bilateralblur.js +3 -1
  22. package/dist/blitter/bilateralblur.js.map +1 -1
  23. package/dist/blitter/blitter.js +3 -3
  24. package/dist/blitter/blitter.js.map +1 -1
  25. package/dist/camera/camera.js +398 -45
  26. package/dist/camera/camera.js.map +1 -1
  27. package/dist/camera/perspectivecamera.js +69 -2
  28. package/dist/camera/perspectivecamera.js.map +1 -1
  29. package/dist/index.d.ts +9100 -8712
  30. package/dist/index.js +9 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/material/blinn.js +1 -1
  33. package/dist/material/blinn.js.map +1 -1
  34. package/dist/material/lambert.js +3 -3
  35. package/dist/material/lambert.js.map +1 -1
  36. package/dist/material/material.js +8 -5
  37. package/dist/material/material.js.map +1 -1
  38. package/dist/material/meshmaterial.js +17 -10
  39. package/dist/material/meshmaterial.js.map +1 -1
  40. package/dist/material/mixins/lightmodel/blinnphong.js +1 -1
  41. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  42. package/dist/material/mixins/lightmodel/lambert.js +1 -1
  43. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  44. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +21 -6
  45. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  46. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +1 -1
  47. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  48. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +1 -1
  49. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  50. package/dist/material/mixins/lit.js +33 -11
  51. package/dist/material/mixins/lit.js.map +1 -1
  52. package/dist/material/mixins/pbr/brdf.js +44 -12
  53. package/dist/material/mixins/pbr/brdf.js.map +1 -1
  54. package/dist/material/mixins/pbr/common.js +149 -14
  55. package/dist/material/mixins/pbr/common.js.map +1 -1
  56. package/dist/material/mtoon.js +7 -4
  57. package/dist/material/mtoon.js.map +1 -1
  58. package/dist/material/pbrblueprint.js +5 -1
  59. package/dist/material/pbrblueprint.js.map +1 -1
  60. package/dist/material/pbrmr.js +1 -1
  61. package/dist/material/pbrmr.js.map +1 -1
  62. package/dist/material/pbrsg.js +1 -1
  63. package/dist/material/pbrsg.js.map +1 -1
  64. package/dist/material/shader/helper.js +186 -11
  65. package/dist/material/shader/helper.js.map +1 -1
  66. package/dist/material/skin.js +2 -2
  67. package/dist/material/skin.js.map +1 -1
  68. package/dist/material/terrain-cm.js +5 -5
  69. package/dist/material/terrain-cm.js.map +1 -1
  70. package/dist/material/water.js +6 -4
  71. package/dist/material/water.js.map +1 -1
  72. package/dist/posteffect/bloom.js +54 -4
  73. package/dist/posteffect/bloom.js.map +1 -1
  74. package/dist/posteffect/compositor.js +41 -0
  75. package/dist/posteffect/compositor.js.map +1 -1
  76. package/dist/posteffect/posteffect.js +51 -0
  77. package/dist/posteffect/posteffect.js.map +1 -1
  78. package/dist/posteffect/sao.js +7 -6
  79. package/dist/posteffect/sao.js.map +1 -1
  80. package/dist/posteffect/skinsss.js +3 -3
  81. package/dist/posteffect/skinsss.js.map +1 -1
  82. package/dist/posteffect/ssgi.js +1196 -0
  83. package/dist/posteffect/ssgi.js.map +1 -0
  84. package/dist/posteffect/ssr.js +83 -45
  85. package/dist/posteffect/ssr.js.map +1 -1
  86. package/dist/posteffect/sss.js +137 -49
  87. package/dist/posteffect/sss.js.map +1 -1
  88. package/dist/posteffect/taa.js +5 -4
  89. package/dist/posteffect/taa.js.map +1 -1
  90. package/dist/posteffect/tonemap.js +15 -3
  91. package/dist/posteffect/tonemap.js.map +1 -1
  92. package/dist/render/abuffer_oit.js +2 -2
  93. package/dist/render/abuffer_oit.js.map +1 -1
  94. package/dist/render/cluster_light.js +12 -6
  95. package/dist/render/cluster_light.js.map +1 -1
  96. package/dist/render/drawable_mixin.js +8 -5
  97. package/dist/render/drawable_mixin.js.map +1 -1
  98. package/dist/render/dualdepthpeeling_oit.js +10 -5
  99. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  100. package/dist/render/envlight.js +298 -5
  101. package/dist/render/envlight.js.map +1 -1
  102. package/dist/render/gpu_picking.js +7 -5
  103. package/dist/render/gpu_picking.js.map +1 -1
  104. package/dist/render/hzb.js +28 -19
  105. package/dist/render/hzb.js.map +1 -1
  106. package/dist/render/lightpass.js +18 -4
  107. package/dist/render/lightpass.js.map +1 -1
  108. package/dist/render/render_queue.js +37 -4
  109. package/dist/render/render_queue.js.map +1 -1
  110. package/dist/render/renderer.js +8 -3
  111. package/dist/render/renderer.js.map +1 -1
  112. package/dist/render/rendergraph/blackboard.js +20 -48
  113. package/dist/render/rendergraph/blackboard.js.map +1 -1
  114. package/dist/render/rendergraph/device_pool_allocator.js +18 -58
  115. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  116. package/dist/render/rendergraph/executor.js +89 -115
  117. package/dist/render/rendergraph/executor.js.map +1 -1
  118. package/dist/render/rendergraph/forward_plus_builder.js +1003 -715
  119. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  120. package/dist/render/rendergraph/frame_graph_context.js +41 -0
  121. package/dist/render/rendergraph/frame_graph_context.js.map +1 -0
  122. package/dist/render/rendergraph/frame_resource_requirements.js +14 -0
  123. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -0
  124. package/dist/render/rendergraph/history_resource_manager.js +74 -140
  125. package/dist/render/rendergraph/history_resource_manager.js.map +1 -1
  126. package/dist/render/rendergraph/history_resources.js +7 -5
  127. package/dist/render/rendergraph/history_resources.js.map +1 -1
  128. package/dist/render/rendergraph/render_context.js +19 -0
  129. package/dist/render/rendergraph/render_context.js.map +1 -0
  130. package/dist/render/rendergraph/render_pipeline.js +315 -0
  131. package/dist/render/rendergraph/render_pipeline.js.map +1 -0
  132. package/dist/render/rendergraph/rendergraph.js +158 -141
  133. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  134. package/dist/render/rendergraph/scene_render_context.js +267 -0
  135. package/dist/render/rendergraph/scene_render_context.js.map +1 -0
  136. package/dist/render/rendergraph/texture_affinity_cache.js +19 -0
  137. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -0
  138. package/dist/render/rendergraph/types.js +19 -31
  139. package/dist/render/rendergraph/types.js.map +1 -1
  140. package/dist/render/renderpass.js +3 -3
  141. package/dist/render/renderpass.js.map +1 -1
  142. package/dist/render/shadow_mask_pass.js +10 -6
  143. package/dist/render/shadow_mask_pass.js.map +1 -1
  144. package/dist/render/sky.js +197 -35
  145. package/dist/render/sky.js.map +1 -1
  146. package/dist/render/weightedblended_oit.js +4 -3
  147. package/dist/render/weightedblended_oit.js.map +1 -1
  148. package/dist/scene/basesprite.js +2 -0
  149. package/dist/scene/basesprite.js.map +1 -1
  150. package/dist/scene/batchgroup.js +10 -0
  151. package/dist/scene/batchgroup.js.map +1 -1
  152. package/dist/scene/environment.js +61 -2
  153. package/dist/scene/environment.js.map +1 -1
  154. package/dist/scene/light.js +178 -10
  155. package/dist/scene/light.js.map +1 -1
  156. package/dist/scene/meshdrawable.js +89 -2
  157. package/dist/scene/meshdrawable.js.map +1 -1
  158. package/dist/scene/msdftext.js +2 -0
  159. package/dist/scene/msdftext.js.map +1 -1
  160. package/dist/scene/msdftextsprite.js +2 -0
  161. package/dist/scene/msdftextsprite.js.map +1 -1
  162. package/dist/scene/particlesys.js +2 -0
  163. package/dist/scene/particlesys.js.map +1 -1
  164. package/dist/scene/scene.js +34 -1
  165. package/dist/scene/scene.js.map +1 -1
  166. package/dist/scene/terrain-cm/grass.js +2 -1
  167. package/dist/scene/terrain-cm/grass.js.map +1 -1
  168. package/dist/scene/terrain-cm/terrain-cm.js +2 -0
  169. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  170. package/dist/scene/water.js +2 -0
  171. package/dist/scene/water.js.map +1 -1
  172. package/dist/shaders/atmosphere.js +5 -4
  173. package/dist/shaders/atmosphere.js.map +1 -1
  174. package/dist/shaders/fog.js +10 -3
  175. package/dist/shaders/fog.js.map +1 -1
  176. package/dist/shaders/shadow.js +96 -29
  177. package/dist/shaders/shadow.js.map +1 -1
  178. package/dist/shaders/ssgi.js +63 -0
  179. package/dist/shaders/ssgi.js.map +1 -0
  180. package/dist/shaders/ssgi_reprojection.js +118 -0
  181. package/dist/shaders/ssgi_reprojection.js.map +1 -0
  182. package/dist/shaders/ssr.js +322 -154
  183. package/dist/shaders/ssr.js.map +1 -1
  184. package/dist/shadow/esm.js +13 -8
  185. package/dist/shadow/esm.js.map +1 -1
  186. package/dist/shadow/pcf_opt.js +15 -12
  187. package/dist/shadow/pcf_opt.js.map +1 -1
  188. package/dist/shadow/pcf_pd.js +15 -12
  189. package/dist/shadow/pcf_pd.js.map +1 -1
  190. package/dist/shadow/pcss.js +17 -11
  191. package/dist/shadow/pcss.js.map +1 -1
  192. package/dist/shadow/shadow_impl.js +20 -2
  193. package/dist/shadow/shadow_impl.js.map +1 -1
  194. package/dist/shadow/shadowmapper.js +10 -3
  195. package/dist/shadow/shadowmapper.js.map +1 -1
  196. package/dist/shadow/ssm.js +16 -13
  197. package/dist/shadow/ssm.js.map +1 -1
  198. package/dist/shadow/vsm.js +10 -8
  199. package/dist/shadow/vsm.js.map +1 -1
  200. package/dist/utility/physical.js +74 -0
  201. package/dist/utility/physical.js.map +1 -0
  202. package/dist/utility/rendermipmap.js +2 -2
  203. package/dist/utility/rendermipmap.js.map +1 -1
  204. package/dist/utility/serialization/manager.js +3 -1
  205. package/dist/utility/serialization/manager.js.map +1 -1
  206. package/dist/utility/serialization/scene/animation.js +3 -1
  207. package/dist/utility/serialization/scene/animation.js.map +1 -1
  208. package/dist/utility/serialization/scene/batch.js +3 -1
  209. package/dist/utility/serialization/scene/batch.js.map +1 -1
  210. package/dist/utility/serialization/scene/camera.js +598 -2
  211. package/dist/utility/serialization/scene/camera.js.map +1 -1
  212. package/dist/utility/serialization/scene/light.js +180 -9
  213. package/dist/utility/serialization/scene/light.js.map +1 -1
  214. package/dist/utility/serialization/scene/material.js +43 -3
  215. package/dist/utility/serialization/scene/material.js.map +1 -1
  216. package/dist/utility/serialization/scene/mesh.js +3 -1
  217. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  218. package/dist/utility/serialization/scene/node.js +3 -1
  219. package/dist/utility/serialization/scene/node.js.map +1 -1
  220. package/dist/utility/serialization/scene/particle.js +3 -1
  221. package/dist/utility/serialization/scene/particle.js.map +1 -1
  222. package/dist/utility/serialization/scene/primitive.js +2 -0
  223. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  224. package/dist/utility/serialization/scene/scene.js +70 -0
  225. package/dist/utility/serialization/scene/scene.js.map +1 -1
  226. package/dist/utility/serialization/scene/sprite.js +3 -1
  227. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  228. package/dist/utility/serialization/scene/terrain.js +3 -1
  229. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  230. package/dist/utility/serialization/scene/text.js +3 -0
  231. package/dist/utility/serialization/scene/text.js.map +1 -1
  232. package/dist/utility/serialization/scene/water.js +3 -1
  233. package/dist/utility/serialization/scene/water.js.map +1 -1
  234. package/dist/values.js +2 -3
  235. package/dist/values.js.map +1 -1
  236. package/package.json +3 -3
  237. package/dist/animation/joint_dynamics/convex_collider.js +0 -320
  238. package/dist/animation/joint_dynamics/convex_collider.js.map +0 -1
@@ -0,0 +1,1196 @@
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';
4
+ import { screenSpaceRayTracing_HiZ, screenSpaceRayTracing_Linear2D } from '../shaders/ssr.js';
5
+ import { ShaderHelper } from '../material/shader/helper.js';
6
+ import '../material/lambert.js';
7
+ import '../material/blinn.js';
8
+ import '../material/unlit.js';
9
+ import '../material/particle.js';
10
+ import '../material/subsurfaceprofile.js';
11
+ import '../material/skin.js';
12
+ import '@zephyr3d/device';
13
+ import '../values.js';
14
+ import '../material/meshmaterial.js';
15
+ import '../material/grassmaterial.js';
16
+ import '../material/terrain-cm.js';
17
+ import '../material/pbrmr.js';
18
+ import '../material/pbrsg.js';
19
+ import '../material/mtoon.js';
20
+ import '../material/pbrblueprint.js';
21
+ import '../material/sprite.js';
22
+ import '../material/sprite_std.js';
23
+ import '../utility/blueprint/material/texture.js';
24
+ import { linearToGamma } from '../shaders/misc.js';
25
+ import { fetchSampler } from '../utility/misc.js';
26
+ import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
27
+
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
40
+ */ class SSGI extends AbstractPostEffect {
41
+ static _tracePrograms = {};
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(){
54
+ super();
55
+ this._layer = PostEffectLayer.opaque;
56
+ }
57
+ /** {@inheritDoc AbstractPostEffect.requireLinearDepthTexture} */ requireLinearDepthTexture(ctx) {
58
+ return !!ctx.SSGI;
59
+ }
60
+ /** {@inheritDoc AbstractPostEffect.requireMotionVectorTexture} */ requireMotionVectorTexture(ctx) {
61
+ return !!ctx.SSGI && ctx.device.type === 'webgpu';
62
+ }
63
+ /** {@inheritDoc AbstractPostEffect.requireHiZTexture} */ requireHiZTexture(ctx) {
64
+ return !!ctx.SSGI && ctx.device.type === 'webgpu';
65
+ }
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) {
72
+ return s.input;
73
+ }
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,
102
+ sizeMode: 'absolute',
103
+ width: traceWidth,
104
+ height: traceHeight
105
+ };
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);
159
+ }
160
+ for (const dep of s.dependencies){
161
+ builder.read(dep);
162
+ }
163
+ };
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
+ }
174
+ const out = builder.createTexture({
175
+ ...traceDesc,
176
+ label: 'SSGI:rawIrradiance'
177
+ });
178
+ const ao = builder.createTexture({
179
+ ...aoTraceDesc,
180
+ label: 'SSGI:rawAO'
181
+ });
182
+ const fb = builder.createFramebuffer({
183
+ label: 'SSGI:traceFB',
184
+ width: traceWidth,
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,
221
+ colorAttachments: out,
222
+ depthAttachment: null,
223
+ ignoreDepthStencil: true
224
+ });
225
+ builder.setExecute((rg)=>{
226
+ const device = ctx.device;
227
+ device.pushDeviceStates();
228
+ try {
229
+ device.setFramebuffer(rg.getFramebuffer(fb));
230
+ this.writeSurface(ctx, rg.getTexture(linearDepthHandle), rg.getTexture(normalHandle));
231
+ } finally{
232
+ device.popDeviceStates();
233
+ }
234
+ });
235
+ return out;
236
+ });
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);
305
+ const out = builder.createTexture({
306
+ ...traceDesc,
307
+ label: `SSGI:atrous${pass}`
308
+ });
309
+ const ao = builder.createTexture({
310
+ ...aoTraceDesc,
311
+ label: `SSGI:atrousAO${pass}`
312
+ });
313
+ const fb = builder.createFramebuffer({
314
+ label: `SSGI:atrousFB${pass}`,
315
+ width: traceWidth,
316
+ height: traceHeight,
317
+ colorAttachments: [
318
+ out,
319
+ ao
320
+ ],
321
+ depthAttachment: null,
322
+ ignoreDepthStencil: true
323
+ });
324
+ builder.setExecute((rg)=>{
325
+ const device = ctx.device;
326
+ device.pushDeviceStates();
327
+ try {
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);
330
+ } finally{
331
+ device.popDeviceStates();
332
+ }
333
+ });
334
+ return {
335
+ irradiance: out,
336
+ ao
337
+ };
338
+ });
339
+ filteredHandle = filtered.irradiance;
340
+ filteredAOHandle = filtered.ao;
341
+ }
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);
354
+ const out = builder.createTexture({
355
+ ...fullIrradianceDesc,
356
+ label: 'SSGI:upsampledIrradiance'
357
+ });
358
+ const ao = builder.createTexture({
359
+ ...fullAODesc,
360
+ label: 'SSGI:upsampledAO'
361
+ });
362
+ const fb = builder.createFramebuffer({
363
+ label: 'SSGI:upsampleFB',
364
+ width: s.width,
365
+ height: s.height,
366
+ colorAttachments: [
367
+ out,
368
+ ao
369
+ ],
370
+ depthAttachment: null,
371
+ ignoreDepthStencil: true
372
+ });
373
+ builder.setExecute((rg)=>{
374
+ const device = ctx.device;
375
+ device.pushDeviceStates();
376
+ try {
377
+ device.setFramebuffer(rg.getFramebuffer(fb));
378
+ this.upsample(ctx, rg.getTexture(source), rg.getTexture(aoSource), rg.getTexture(linearDepthHandle), rg.getTexture(normalHandle));
379
+ } finally{
380
+ device.popDeviceStates();
381
+ }
382
+ });
383
+ return {
384
+ irradiance: out,
385
+ ao
386
+ };
387
+ });
388
+ finalIrradianceHandle = upsampled.irradiance;
389
+ finalAOHandle = upsampled.ao;
390
+ }
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)=>{
399
+ builder.read(s.input);
400
+ if (noFogHandle) {
401
+ builder.read(noFogHandle);
402
+ }
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);
411
+ builder.setExecute((rg)=>{
412
+ const device = ctx.device;
413
+ device.pushDeviceStates();
414
+ try {
415
+ device.setFramebuffer(output.framebuffer ? rg.getFramebuffer(output.framebuffer) : null);
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));
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);
439
+ } finally{
440
+ device.popDeviceStates();
441
+ }
442
+ });
443
+ return output.color;
444
+ });
445
+ }
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);
456
+ }
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'));
480
+ }
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));
489
+ }
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];
514
+ if (!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;
530
+ }
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;
554
+ }
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);
565
+ this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
566
+ }
567
+ /** @internal */ writeSurface(ctx, depth, normal) {
568
+ let program = SSGI._surfaceProgram;
569
+ if (!program) {
570
+ program = this.createSurfaceProgram(ctx);
571
+ SSGI._surfaceProgram = program;
572
+ }
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;
581
+ }
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);
586
+ this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
587
+ }
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;
593
+ }
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;
604
+ }
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'));
610
+ }
611
+ /** @internal */ createTraceProgram(ctx, useHiZ, sampleHistory) {
612
+ const raysPerPixel = ctx.camera.ssgiResolvedSettings.raysPerPixel;
613
+ const program = ctx.device.buildRenderProgram({
614
+ vertex (pb) {
615
+ this.flip = pb.int().uniform(0);
616
+ this.$inputs.pos = pb.vec2().attrib('position');
617
+ this.$outputs.uv = pb.vec2();
618
+ pb.main(function() {
619
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
620
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
621
+ this.$if(pb.notEqual(this.flip, 0), function() {
622
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
623
+ });
624
+ });
625
+ },
626
+ fragment (pb) {
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);
632
+ this.normalTex = 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
+ }
646
+ this.cameraNearFar = pb.vec2().uniform(0);
647
+ this.viewMatrix = pb.mat4().uniform(0);
648
+ this.invViewMatrix = pb.mat4().uniform(0);
649
+ this.projMatrix = pb.mat4().uniform(0);
650
+ this.invProjMatrix = pb.mat4().uniform(0);
651
+ this.traceParams = pb.vec4().uniform(0);
652
+ this.targetSize = pb.vec4().uniform(0);
653
+ this.radianceParams = pb.vec4().uniform(0);
654
+ this.skyOcclusion = pb.float().uniform(0);
655
+ ctx.env.light.envLight.initShaderBindings(pb);
656
+ this.$outputs.outColor = pb.vec4();
657
+ this.$outputs.outAO = pb.vec4();
658
+ pb.func('SSGI_getPosition', [
659
+ pb.vec2('uv')
660
+ ], function() {
661
+ this.$l.linearDepth = ShaderHelper.sampleLinearDepth(this, this.depthTex, this.uv, 0);
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))));
685
+ });
686
+ pb.main(function() {
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)));
697
+ this.$l.viewNormal = pb.normalize(pb.mul(this.viewMatrix, pb.vec4(this.worldNormal, 0)).xyz);
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);
732
+ } else {
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);
734
+ }
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);
790
+ });
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));
814
+ });
815
+ });
816
+ }
817
+ });
818
+ program.name = '@SSGI_Trace';
819
+ return program;
820
+ }
821
+ /** @internal */ createTemporalProgram(ctx, hasHistory) {
822
+ const program = ctx.device.buildRenderProgram({
823
+ vertex (pb) {
824
+ this.flip = pb.int().uniform(0);
825
+ this.$inputs.pos = pb.vec2().attrib('position');
826
+ this.$outputs.uv = pb.vec2();
827
+ pb.main(function() {
828
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
829
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
830
+ this.$if(pb.notEqual(this.flip, 0), function() {
831
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
832
+ });
833
+ });
834
+ },
835
+ fragment (pb) {
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
+ });
858
+ pb.main(function() {
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;
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);
891
+ }
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);
1050
+ });
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
+ });
1065
+ }
1066
+ });
1067
+ program.name = '@SSGI_Surface';
1068
+ return program;
1069
+ }
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) {
1102
+ const program = ctx.device.buildRenderProgram({
1103
+ vertex (pb) {
1104
+ this.flip = pb.int().uniform(0);
1105
+ this.$inputs.pos = pb.vec2().attrib('position');
1106
+ this.$outputs.uv = pb.vec2();
1107
+ pb.main(function() {
1108
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
1109
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
1110
+ this.$if(pb.notEqual(this.flip, 0), function() {
1111
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
1112
+ });
1113
+ });
1114
+ },
1115
+ fragment (pb) {
1116
+ this.colorTex = pb.tex2D().uniform(0);
1117
+ this.aoTex = pb.tex2D().uniform(0);
1118
+ this.aoParams = pb.vec2().uniform(0);
1119
+ this.$outputs.outColor = pb.vec4();
1120
+ pb.main(function() {
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);
1147
+ });
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
+ });
1188
+ }
1189
+ });
1190
+ program.name = '@SSGI_Upsample';
1191
+ return program;
1192
+ }
1193
+ }
1194
+
1195
+ export { SSGI };
1196
+ //# sourceMappingURL=ssgi.js.map