@holoscript/engine 6.0.3 → 6.0.4

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 (192) hide show
  1. package/dist/AutoMesher-CK47F6AV.js +17 -0
  2. package/dist/GPUBuffers-2LHBCD7X.js +9 -0
  3. package/dist/WebGPUContext-TNEUYU2Y.js +11 -0
  4. package/dist/animation/index.cjs +38 -38
  5. package/dist/animation/index.d.cts +1 -1
  6. package/dist/animation/index.d.ts +1 -1
  7. package/dist/animation/index.js +1 -1
  8. package/dist/audio/index.cjs +16 -6
  9. package/dist/audio/index.d.cts +1 -1
  10. package/dist/audio/index.d.ts +1 -1
  11. package/dist/audio/index.js +1 -1
  12. package/dist/camera/index.cjs +23 -23
  13. package/dist/camera/index.d.cts +1 -1
  14. package/dist/camera/index.d.ts +1 -1
  15. package/dist/camera/index.js +1 -1
  16. package/dist/character/index.cjs +6 -4
  17. package/dist/character/index.js +1 -1
  18. package/dist/choreography/index.cjs +1194 -0
  19. package/dist/choreography/index.d.cts +687 -0
  20. package/dist/choreography/index.d.ts +687 -0
  21. package/dist/choreography/index.js +1156 -0
  22. package/dist/chunk-2CSNRI2N.js +217 -0
  23. package/dist/chunk-33T2WINR.js +266 -0
  24. package/dist/chunk-35R73OFM.js +1257 -0
  25. package/dist/chunk-4MMDSUNP.js +1256 -0
  26. package/dist/chunk-5V6HOU72.js +319 -0
  27. package/dist/chunk-6QOP6PYF.js +1038 -0
  28. package/dist/chunk-7KMJVHIL.js +8944 -0
  29. package/dist/chunk-7VPUC62U.js +1106 -0
  30. package/dist/chunk-A2Y6RCAT.js +1878 -0
  31. package/dist/chunk-AHM42MK6.js +8944 -0
  32. package/dist/chunk-BL7IDTHE.js +218 -0
  33. package/dist/chunk-CITOMSWL.js +10462 -0
  34. package/dist/chunk-CXDPKW2K.js +8944 -0
  35. package/dist/chunk-CXZPLD4S.js +223 -0
  36. package/dist/chunk-CZYJE7IH.js +5169 -0
  37. package/dist/chunk-D2OP7YC7.js +6325 -0
  38. package/dist/chunk-EDRVQHUU.js +1544 -0
  39. package/dist/chunk-EJSLOOW2.js +3589 -0
  40. package/dist/chunk-F53SFGW5.js +1878 -0
  41. package/dist/chunk-HCFPELPY.js +919 -0
  42. package/dist/chunk-HNEE36PY.js +93 -0
  43. package/dist/chunk-HYXNV36F.js +1256 -0
  44. package/dist/chunk-IB7KHVFY.js +821 -0
  45. package/dist/chunk-IBBO7YYG.js +690 -0
  46. package/dist/chunk-ILIBGINU.js +5470 -0
  47. package/dist/chunk-IS4MHLKN.js +5479 -0
  48. package/dist/chunk-JT2PFKWD.js +5479 -0
  49. package/dist/chunk-K4CUB4NY.js +1038 -0
  50. package/dist/chunk-KATDQXRJ.js +10462 -0
  51. package/dist/chunk-KBQE6ZFJ.js +8944 -0
  52. package/dist/chunk-KBVD5K7E.js +560 -0
  53. package/dist/chunk-KCDPVQRY.js +4088 -0
  54. package/dist/chunk-KN4QJPKN.js +8944 -0
  55. package/dist/chunk-KWJ3ROSI.js +8944 -0
  56. package/dist/chunk-L45VF6DD.js +919 -0
  57. package/dist/chunk-LY4T37YK.js +307 -0
  58. package/dist/chunk-MDN5WZXA.js +1544 -0
  59. package/dist/chunk-MGCDP6VU.js +928 -0
  60. package/dist/chunk-NCX7X6G2.js +8681 -0
  61. package/dist/chunk-OF54BPVD.js +913 -0
  62. package/dist/chunk-OWSN2Q3Q.js +690 -0
  63. package/dist/chunk-PRRB5TTA.js +406 -0
  64. package/dist/chunk-PXWVQF76.js +4086 -0
  65. package/dist/chunk-PYCOIDT2.js +812 -0
  66. package/dist/chunk-PZCSADOV.js +928 -0
  67. package/dist/chunk-Q2XBVS2K.js +1038 -0
  68. package/dist/chunk-QDZRXWN5.js +1776 -0
  69. package/dist/chunk-RNWOZ6WQ.js +913 -0
  70. package/dist/chunk-ROLFT4CJ.js +1693 -0
  71. package/dist/chunk-SLTJRZ2N.js +266 -0
  72. package/dist/chunk-SRUS5XSU.js +4088 -0
  73. package/dist/chunk-TKCA3WZ5.js +5409 -0
  74. package/dist/chunk-TNRMXYI2.js +1650 -0
  75. package/dist/chunk-TQB3GJGM.js +9763 -0
  76. package/dist/chunk-TUFGXG6K.js +510 -0
  77. package/dist/chunk-U6KMTGQJ.js +632 -0
  78. package/dist/chunk-VMGJQST6.js +8681 -0
  79. package/dist/chunk-X4F4TCG4.js +5470 -0
  80. package/dist/chunk-ZIFROE75.js +1544 -0
  81. package/dist/chunk-ZIJQYHSQ.js +1204 -0
  82. package/dist/combat/index.cjs +4 -4
  83. package/dist/combat/index.d.cts +1 -1
  84. package/dist/combat/index.d.ts +1 -1
  85. package/dist/combat/index.js +1 -1
  86. package/dist/ecs/index.cjs +1 -1
  87. package/dist/ecs/index.js +1 -1
  88. package/dist/environment/index.cjs +14 -14
  89. package/dist/environment/index.d.cts +1 -1
  90. package/dist/environment/index.d.ts +1 -1
  91. package/dist/environment/index.js +1 -1
  92. package/dist/gpu/index.cjs +4810 -0
  93. package/dist/gpu/index.js +3714 -0
  94. package/dist/hologram/index.cjs +27 -1
  95. package/dist/hologram/index.js +1 -1
  96. package/dist/index-B2PIsAmR.d.cts +2180 -0
  97. package/dist/index-B2PIsAmR.d.ts +2180 -0
  98. package/dist/index-BHySEPX7.d.cts +2921 -0
  99. package/dist/index-BJV21zuy.d.cts +341 -0
  100. package/dist/index-BJV21zuy.d.ts +341 -0
  101. package/dist/index-BQutTphC.d.cts +790 -0
  102. package/dist/index-ByIq2XrS.d.cts +3910 -0
  103. package/dist/index-BysHjDSO.d.cts +224 -0
  104. package/dist/index-BysHjDSO.d.ts +224 -0
  105. package/dist/index-CKwAJGck.d.ts +455 -0
  106. package/dist/index-CUl3QstQ.d.cts +3006 -0
  107. package/dist/index-CUl3QstQ.d.ts +3006 -0
  108. package/dist/index-CmYtNiI-.d.cts +953 -0
  109. package/dist/index-CmYtNiI-.d.ts +953 -0
  110. package/dist/index-CnRzWxi_.d.cts +522 -0
  111. package/dist/index-CnRzWxi_.d.ts +522 -0
  112. package/dist/index-CwRWbSC7.d.ts +2921 -0
  113. package/dist/index-CxKIBstO.d.ts +790 -0
  114. package/dist/index-DJ6-R8vh.d.cts +455 -0
  115. package/dist/index-DQKisbcI.d.cts +4968 -0
  116. package/dist/index-DQKisbcI.d.ts +4968 -0
  117. package/dist/index-DRT2zJez.d.ts +3910 -0
  118. package/dist/index-DfNLiAka.d.cts +192 -0
  119. package/dist/index-DfNLiAka.d.ts +192 -0
  120. package/dist/index-nMvkoRm8.d.cts +405 -0
  121. package/dist/index-nMvkoRm8.d.ts +405 -0
  122. package/dist/index-s9yOFU37.d.cts +604 -0
  123. package/dist/index-s9yOFU37.d.ts +604 -0
  124. package/dist/index.cjs +22966 -6960
  125. package/dist/index.d.cts +864 -20
  126. package/dist/index.d.ts +864 -20
  127. package/dist/index.js +3062 -48
  128. package/dist/input/index.cjs +1 -1
  129. package/dist/input/index.js +1 -1
  130. package/dist/orbital/index.cjs +3 -3
  131. package/dist/orbital/index.d.cts +1 -1
  132. package/dist/orbital/index.d.ts +1 -1
  133. package/dist/orbital/index.js +1 -1
  134. package/dist/particles/index.cjs +16 -16
  135. package/dist/particles/index.d.cts +1 -1
  136. package/dist/particles/index.d.ts +1 -1
  137. package/dist/particles/index.js +1 -1
  138. package/dist/physics/index.cjs +2377 -21
  139. package/dist/physics/index.d.cts +1 -1
  140. package/dist/physics/index.d.ts +1 -1
  141. package/dist/physics/index.js +35 -1
  142. package/dist/postfx/index.cjs +3491 -0
  143. package/dist/postfx/index.js +93 -0
  144. package/dist/procedural/index.cjs +1 -1
  145. package/dist/procedural/index.js +1 -1
  146. package/dist/puppeteer-5VF6KDVO.js +52197 -0
  147. package/dist/puppeteer-IZVZ3SG4.js +52197 -0
  148. package/dist/rendering/index.cjs +33 -32
  149. package/dist/rendering/index.d.cts +1 -1
  150. package/dist/rendering/index.d.ts +1 -1
  151. package/dist/rendering/index.js +8 -6
  152. package/dist/runtime/index.cjs +23 -13
  153. package/dist/runtime/index.d.cts +1 -1
  154. package/dist/runtime/index.d.ts +1 -1
  155. package/dist/runtime/index.js +8 -6
  156. package/dist/runtime/protocols/index.cjs +349 -0
  157. package/dist/runtime/protocols/index.js +15 -0
  158. package/dist/scene/index.cjs +8 -8
  159. package/dist/scene/index.d.cts +1 -1
  160. package/dist/scene/index.d.ts +1 -1
  161. package/dist/scene/index.js +1 -1
  162. package/dist/shader/index.cjs +3087 -0
  163. package/dist/shader/index.js +3044 -0
  164. package/dist/simulation/index.cjs +10680 -0
  165. package/dist/simulation/index.d.cts +3 -0
  166. package/dist/simulation/index.d.ts +3 -0
  167. package/dist/simulation/index.js +307 -0
  168. package/dist/spatial/index.cjs +2443 -0
  169. package/dist/spatial/index.d.cts +1545 -0
  170. package/dist/spatial/index.d.ts +1545 -0
  171. package/dist/spatial/index.js +2400 -0
  172. package/dist/terrain/index.cjs +1 -1
  173. package/dist/terrain/index.d.cts +1 -1
  174. package/dist/terrain/index.d.ts +1 -1
  175. package/dist/terrain/index.js +1 -1
  176. package/dist/transformers.node-4NKAPD5U.js +45620 -0
  177. package/dist/vm/index.cjs +7 -8
  178. package/dist/vm/index.d.cts +1 -1
  179. package/dist/vm/index.d.ts +1 -1
  180. package/dist/vm/index.js +1 -1
  181. package/dist/vm-bridge/index.cjs +2 -2
  182. package/dist/vm-bridge/index.d.cts +2 -2
  183. package/dist/vm-bridge/index.d.ts +2 -2
  184. package/dist/vm-bridge/index.js +1 -1
  185. package/dist/vr/index.cjs +6 -6
  186. package/dist/vr/index.js +1 -1
  187. package/dist/world/index.cjs +3 -3
  188. package/dist/world/index.d.cts +1 -1
  189. package/dist/world/index.d.ts +1 -1
  190. package/dist/world/index.js +1 -1
  191. package/package.json +53 -21
  192. package/LICENSE +0 -21
@@ -0,0 +1,3491 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/postfx/index.ts
21
+ var postfx_exports = {};
22
+ __export(postfx_exports, {
23
+ BLIT_SHADER: () => BLIT_SHADER,
24
+ BLOOM_SHADER: () => BLOOM_SHADER,
25
+ CAUSTICS_SHADER: () => CAUSTICS_SHADER,
26
+ CHROMATIC_ABERRATION_SHADER: () => CHROMATIC_ABERRATION_SHADER,
27
+ COLOR_GRADE_SHADER: () => COLOR_GRADE_SHADER,
28
+ CausticsEffect: () => CausticsEffect,
29
+ ChromaticAberrationEffect: () => ChromaticAberrationEffect,
30
+ DEFAULT_PARAMS: () => DEFAULT_PARAMS,
31
+ DEFAULT_PIPELINE_CONFIG: () => DEFAULT_PIPELINE_CONFIG,
32
+ DOF_SHADER: () => DOF_SHADER,
33
+ FILM_GRAIN_SHADER: () => FILM_GRAIN_SHADER,
34
+ FOG_SHADER: () => FOG_SHADER,
35
+ FULLSCREEN_VERTEX_SHADER: () => FULLSCREEN_VERTEX_SHADER,
36
+ FXAAEffect: () => FXAAEffect,
37
+ FXAA_SHADER: () => FXAA_SHADER,
38
+ FilmGrainEffect: () => FilmGrainEffect,
39
+ MOTION_BLUR_SHADER: () => MOTION_BLUR_SHADER,
40
+ PP_PRESETS: () => PP_PRESETS,
41
+ PostProcessEffect: () => PostProcessEffect,
42
+ PostProcessPipeline: () => PostProcessPipeline,
43
+ PostProcessStack: () => PostProcessStack,
44
+ PostProcessingStack: () => PostProcessingStack,
45
+ SHADER_UTILS: () => SHADER_UTILS,
46
+ SHARPEN_SHADER: () => SHARPEN_SHADER,
47
+ SSAOEffect: () => SSAOEffect,
48
+ SSAO_SHADER: () => SSAO_SHADER,
49
+ SSGIEffect: () => SSGIEffect,
50
+ SSGI_SHADER: () => SSGI_SHADER,
51
+ SSREffect: () => SSREffect,
52
+ SSR_SHADER: () => SSR_SHADER,
53
+ SharpenEffect: () => SharpenEffect,
54
+ TONEMAP_SHADER: () => TONEMAP_SHADER,
55
+ ToneMapEffect: () => ToneMapEffect,
56
+ UNIFORM_SIZES: () => UNIFORM_SIZES,
57
+ VIGNETTE_SHADER: () => VIGNETTE_SHADER,
58
+ VignetteEffect: () => VignetteEffect,
59
+ buildEffectShader: () => buildEffectShader,
60
+ createEffect: () => createEffect,
61
+ createHDRPipeline: () => createHDRPipeline,
62
+ createLDRPipeline: () => createLDRPipeline,
63
+ createPostProcessPipeline: () => createPostProcessPipeline,
64
+ getDefaultParams: () => getDefaultParams,
65
+ mergeParams: () => mergeParams,
66
+ validateParams: () => validateParams
67
+ });
68
+ module.exports = __toCommonJS(postfx_exports);
69
+
70
+ // src/rendering/postprocess/PostProcessTypes.ts
71
+ var DEFAULT_PARAMS = {
72
+ bloom: {
73
+ enabled: true,
74
+ intensity: 1,
75
+ threshold: 1,
76
+ softThreshold: 0.5,
77
+ radius: 4,
78
+ iterations: 5,
79
+ anamorphic: 0,
80
+ highQuality: false,
81
+ blendMode: "add"
82
+ },
83
+ tonemap: {
84
+ enabled: true,
85
+ intensity: 1,
86
+ operator: "aces",
87
+ exposure: 1,
88
+ gamma: 2.2,
89
+ whitePoint: 1,
90
+ contrast: 1,
91
+ saturation: 1
92
+ },
93
+ dof: {
94
+ enabled: false,
95
+ intensity: 1,
96
+ focusDistance: 10,
97
+ focalLength: 50,
98
+ aperture: 2.8,
99
+ maxBlur: 1,
100
+ bokehShape: "circle",
101
+ bokehQuality: "medium",
102
+ nearBlur: true
103
+ },
104
+ motionBlur: {
105
+ enabled: false,
106
+ intensity: 1,
107
+ samples: 8,
108
+ velocityScale: 1,
109
+ maxVelocity: 64
110
+ },
111
+ ssao: {
112
+ enabled: false,
113
+ intensity: 1,
114
+ radius: 0.5,
115
+ bias: 0.025,
116
+ samples: 16,
117
+ power: 2,
118
+ falloff: 1,
119
+ mode: "hemisphere",
120
+ bentNormals: false,
121
+ spatialDenoise: false
122
+ },
123
+ fxaa: {
124
+ enabled: true,
125
+ intensity: 1,
126
+ quality: "high",
127
+ edgeThreshold: 0.166,
128
+ edgeThresholdMin: 0.0833
129
+ },
130
+ sharpen: {
131
+ enabled: false,
132
+ intensity: 0.5,
133
+ amount: 0.3,
134
+ threshold: 0.1
135
+ },
136
+ vignette: {
137
+ enabled: false,
138
+ intensity: 0.5,
139
+ roundness: 1,
140
+ smoothness: 0.5,
141
+ color: [0, 0, 0]
142
+ },
143
+ colorGrade: {
144
+ enabled: false,
145
+ intensity: 1,
146
+ shadows: [0, 0, 0],
147
+ midtones: [0, 0, 0],
148
+ highlights: [0, 0, 0],
149
+ shadowsOffset: 0,
150
+ highlightsOffset: 0,
151
+ hueShift: 0,
152
+ temperature: 0,
153
+ tint: 0,
154
+ lutIntensity: 1
155
+ },
156
+ filmGrain: {
157
+ enabled: false,
158
+ intensity: 0.1,
159
+ size: 1.6,
160
+ luminanceContribution: 0.8,
161
+ animated: true
162
+ },
163
+ chromaticAberration: {
164
+ enabled: false,
165
+ intensity: 0.5,
166
+ redOffset: [0.01, 0],
167
+ greenOffset: [0, 0],
168
+ blueOffset: [-0.01, 0],
169
+ radial: true
170
+ },
171
+ fog: {
172
+ enabled: false,
173
+ intensity: 1,
174
+ color: [0.7, 0.8, 0.9],
175
+ density: 0.02,
176
+ start: 10,
177
+ end: 100,
178
+ height: 0,
179
+ heightFalloff: 1,
180
+ mode: "exponential"
181
+ },
182
+ caustics: {
183
+ enabled: false,
184
+ intensity: 0.8,
185
+ scale: 8,
186
+ speed: 0.5,
187
+ color: [0.2, 0.5, 0.8],
188
+ depthFade: 0.5,
189
+ waterLevel: 0.5,
190
+ dispersion: 0,
191
+ foamIntensity: 0,
192
+ shadowStrength: 0
193
+ },
194
+ ssr: {
195
+ enabled: false,
196
+ intensity: 0.8,
197
+ maxSteps: 64,
198
+ stepSize: 0.05,
199
+ thickness: 0.1,
200
+ roughnessFade: 0.5,
201
+ edgeFade: 4,
202
+ roughnessBlur: 0,
203
+ fresnelStrength: 0
204
+ },
205
+ ssgi: {
206
+ enabled: false,
207
+ intensity: 0.5,
208
+ radius: 2,
209
+ samples: 16,
210
+ bounceIntensity: 1,
211
+ falloff: 1,
212
+ temporalBlend: 0,
213
+ spatialDenoise: false,
214
+ multiBounce: 0
215
+ },
216
+ custom: {
217
+ enabled: true,
218
+ intensity: 1,
219
+ shader: "",
220
+ uniforms: {}
221
+ }
222
+ };
223
+ function getDefaultParams(type) {
224
+ return { ...DEFAULT_PARAMS[type] };
225
+ }
226
+ function mergeParams(type, partial) {
227
+ return { ...DEFAULT_PARAMS[type], ...partial };
228
+ }
229
+ function validateParams(type, params) {
230
+ const errors = [];
231
+ if (params == null) {
232
+ return { valid: false, errors: ["params is null or undefined"] };
233
+ }
234
+ if (typeof params.enabled !== "boolean") {
235
+ errors.push("enabled must be boolean");
236
+ }
237
+ if (typeof params.intensity !== "number" || params.intensity < 0) {
238
+ errors.push("intensity must be non-negative number");
239
+ }
240
+ switch (type) {
241
+ case "bloom": {
242
+ const p = params;
243
+ if (p.threshold < 0) errors.push("bloom.threshold must be >= 0");
244
+ if (p.iterations < 1 || p.iterations > 16) {
245
+ errors.push("bloom.iterations must be 1-16");
246
+ }
247
+ break;
248
+ }
249
+ case "tonemap": {
250
+ const p = params;
251
+ if (p.exposure <= 0) errors.push("tonemap.exposure must be > 0");
252
+ if (p.gamma <= 0) errors.push("tonemap.gamma must be > 0");
253
+ break;
254
+ }
255
+ case "dof": {
256
+ const p = params;
257
+ if (p.focusDistance <= 0) errors.push("dof.focusDistance must be > 0");
258
+ if (p.aperture <= 0) errors.push("dof.aperture must be > 0");
259
+ break;
260
+ }
261
+ case "ssao": {
262
+ const p = params;
263
+ if (p.samples < 4 || p.samples > 64) {
264
+ errors.push("ssao.samples must be 4-64");
265
+ }
266
+ if (p.radius <= 0) errors.push("ssao.radius must be > 0");
267
+ break;
268
+ }
269
+ }
270
+ return { valid: errors.length === 0, errors };
271
+ }
272
+ var UNIFORM_SIZES = {
273
+ bloom: 48,
274
+ // intensity, threshold, softThreshold, radius, iterations, anamorphic
275
+ tonemap: 32,
276
+ // operator, exposure, gamma, whitePoint, contrast, saturation
277
+ dof: 48,
278
+ // focusDistance, focalLength, aperture, maxBlur, near/far
279
+ motionBlur: 16,
280
+ // samples, velocityScale, maxVelocity
281
+ ssao: 48,
282
+ // radius, bias, samples, power, falloff, mode, bentNormals, spatialDenoise
283
+ fxaa: 16,
284
+ // quality, edgeThreshold, edgeThresholdMin
285
+ sharpen: 16,
286
+ // amount, threshold
287
+ vignette: 32,
288
+ // intensity, roundness, smoothness, color
289
+ colorGrade: 96,
290
+ // shadows, midtones, highlights, offsets, hue, temp, tint
291
+ filmGrain: 16,
292
+ // size, luminance, time
293
+ chromaticAberration: 32,
294
+ // offsets, radial
295
+ fog: 48,
296
+ // color, density, start, end, height, falloff
297
+ caustics: 64,
298
+ // intensity, scale, speed, time, color, depthFade, waterLevel, dispersion, foam, shadow
299
+ ssr: 48,
300
+ // maxSteps, stepSize, thickness, roughnessFade, edgeFade, intensity, roughnessBlur, fresnel
301
+ ssgi: 48,
302
+ // radius, samples, bounceIntensity, falloff, time, intensity, temporalBlend, denoise, multiBounce
303
+ custom: 256
304
+ // generic uniform buffer for custom effects
305
+ };
306
+
307
+ // src/rendering/postprocess/PostProcessShaders.ts
308
+ var FULLSCREEN_VERTEX_SHADER = (
309
+ /* wgsl */
310
+ `
311
+ struct VertexOutput {
312
+ @builtin(position) position: vec4f,
313
+ @location(0) uv: vec2f,
314
+ }
315
+
316
+ @vertex
317
+ fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
318
+ // Generate fullscreen triangle
319
+ var positions = array<vec2f, 3>(
320
+ vec2f(-1.0, -1.0),
321
+ vec2f(3.0, -1.0),
322
+ vec2f(-1.0, 3.0)
323
+ );
324
+
325
+ var uvs = array<vec2f, 3>(
326
+ vec2f(0.0, 1.0),
327
+ vec2f(2.0, 1.0),
328
+ vec2f(0.0, -1.0)
329
+ );
330
+
331
+ var output: VertexOutput;
332
+ output.position = vec4f(positions[vertexIndex], 0.0, 1.0);
333
+ output.uv = uvs[vertexIndex];
334
+ return output;
335
+ }
336
+ `
337
+ );
338
+ var SHADER_UTILS = (
339
+ /* wgsl */
340
+ `
341
+ // Luminance calculation (Rec. 709)
342
+ fn luminance(color: vec3f) -> f32 {
343
+ return dot(color, vec3f(0.2126, 0.7152, 0.0722));
344
+ }
345
+
346
+ // sRGB to linear conversion
347
+ fn srgbToLinear(color: vec3f) -> vec3f {
348
+ return pow(color, vec3f(2.2));
349
+ }
350
+
351
+ // Linear to sRGB conversion
352
+ fn linearToSrgb(color: vec3f) -> vec3f {
353
+ return pow(color, vec3f(1.0 / 2.2));
354
+ }
355
+
356
+ // Hash function for noise
357
+ fn hash(p: vec2f) -> f32 {
358
+ let k = vec2f(0.3183099, 0.3678794);
359
+ let x = p * k + k.yx;
360
+ return fract(16.0 * k.x * fract(x.x * x.y * (x.x + x.y)));
361
+ }
362
+
363
+ // Simple 2D noise
364
+ fn noise2D(p: vec2f) -> f32 {
365
+ let i = floor(p);
366
+ let f = fract(p);
367
+ let u = f * f * (3.0 - 2.0 * f);
368
+ return mix(
369
+ mix(hash(i + vec2f(0.0, 0.0)), hash(i + vec2f(1.0, 0.0)), u.x),
370
+ mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), u.x),
371
+ u.y
372
+ );
373
+ }
374
+ `
375
+ );
376
+ var BLOOM_SHADER = (
377
+ /* wgsl */
378
+ `
379
+ ${SHADER_UTILS}
380
+
381
+ struct BloomUniforms {
382
+ intensity: f32,
383
+ threshold: f32,
384
+ softThreshold: f32,
385
+ radius: f32,
386
+ iterations: f32,
387
+ anamorphic: f32,
388
+ highQuality: f32,
389
+ padding: f32,
390
+ time: f32,
391
+ deltaTime: f32,
392
+ padding2: vec2f,
393
+ }
394
+
395
+ @group(0) @binding(0) var texSampler: sampler;
396
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
397
+ @group(0) @binding(2) var<uniform> uniforms: BloomUniforms;
398
+
399
+ // Threshold with soft knee
400
+ fn softThreshold(color: vec3f) -> vec3f {
401
+ let brightness = max(max(color.r, color.g), color.b);
402
+ var soft = brightness - uniforms.threshold + uniforms.softThreshold;
403
+ soft = clamp(soft, 0.0, 2.0 * uniforms.softThreshold);
404
+ soft = soft * soft / (4.0 * uniforms.softThreshold + 0.00001);
405
+ var contribution = max(soft, brightness - uniforms.threshold);
406
+ contribution /= max(brightness, 0.00001);
407
+ return color * contribution;
408
+ }
409
+
410
+ // 9-tap gaussian blur
411
+ fn blur9(uv: vec2f, direction: vec2f) -> vec3f {
412
+ let texSize = vec2f(textureDimensions(inputTexture));
413
+ let offset = direction / texSize;
414
+
415
+ var color = textureSample(inputTexture, texSampler, uv).rgb * 0.2270270270;
416
+ color += textureSample(inputTexture, texSampler, uv + offset * 1.3846153846).rgb * 0.3162162162;
417
+ color += textureSample(inputTexture, texSampler, uv - offset * 1.3846153846).rgb * 0.3162162162;
418
+ color += textureSample(inputTexture, texSampler, uv + offset * 3.2307692308).rgb * 0.0702702703;
419
+ color += textureSample(inputTexture, texSampler, uv - offset * 3.2307692308).rgb * 0.0702702703;
420
+
421
+ return color;
422
+ }
423
+
424
+ @fragment
425
+ fn fs_bloom(input: VertexOutput) -> @location(0) vec4f {
426
+ let color = textureSample(inputTexture, texSampler, input.uv).rgb;
427
+
428
+ // Extract bright pixels with soft threshold
429
+ var bloom = softThreshold(color);
430
+
431
+ // Simple blur approximation (in production, use multi-pass)
432
+ let texSize = vec2f(textureDimensions(inputTexture));
433
+ let radius = uniforms.radius / texSize;
434
+
435
+ var blurred = bloom;
436
+ for (var i = 0u; i < 4u; i++) {
437
+ let angle = f32(i) * 1.5707963268;
438
+ let offset = vec2f(cos(angle), sin(angle)) * radius;
439
+ blurred += textureSample(inputTexture, texSampler, input.uv + offset).rgb;
440
+ blurred += textureSample(inputTexture, texSampler, input.uv - offset).rgb;
441
+ }
442
+ blurred /= 9.0;
443
+
444
+ // Composite bloom
445
+ let result = color + softThreshold(blurred) * uniforms.intensity;
446
+
447
+ return vec4f(result, 1.0);
448
+ }
449
+ `
450
+ );
451
+ var TONEMAP_SHADER = (
452
+ /* wgsl */
453
+ `
454
+ ${SHADER_UTILS}
455
+
456
+ struct ToneMapUniforms {
457
+ operator: f32,
458
+ exposure: f32,
459
+ gamma: f32,
460
+ whitePoint: f32,
461
+ contrast: f32,
462
+ saturation: f32,
463
+ intensity: f32,
464
+ padding: f32,
465
+ }
466
+
467
+ @group(0) @binding(0) var texSampler: sampler;
468
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
469
+ @group(0) @binding(2) var<uniform> uniforms: ToneMapUniforms;
470
+
471
+ // Reinhard tone mapping
472
+ fn tonemapReinhard(x: vec3f) -> vec3f {
473
+ return x / (x + vec3f(1.0));
474
+ }
475
+
476
+ // Reinhard with luminance-based mapping
477
+ fn tonemapReinhardLum(x: vec3f) -> vec3f {
478
+ let l = luminance(x);
479
+ let nl = l / (l + 1.0);
480
+ return x * (nl / l);
481
+ }
482
+
483
+ // ACES filmic tone mapping
484
+ fn tonemapACES(x: vec3f) -> vec3f {
485
+ let a = 2.51;
486
+ let b = 0.03;
487
+ let c = 2.43;
488
+ let d = 0.59;
489
+ let e = 0.14;
490
+ return clamp((x * (a * x + vec3f(b))) / (x * (c * x + vec3f(d)) + vec3f(e)), vec3f(0.0), vec3f(1.0));
491
+ }
492
+
493
+ // ACES approximation (cheaper)
494
+ fn tonemapACESApprox(x: vec3f) -> vec3f {
495
+ let v = x * 0.6;
496
+ let a = v * (v * 2.51 + 0.03);
497
+ let b = v * (v * 2.43 + 0.59) + 0.14;
498
+ return clamp(a / b, vec3f(0.0), vec3f(1.0));
499
+ }
500
+
501
+ // Uncharted 2 filmic
502
+ fn uncharted2Partial(x: vec3f) -> vec3f {
503
+ let A = 0.15;
504
+ let B = 0.50;
505
+ let C = 0.10;
506
+ let D = 0.20;
507
+ let E = 0.02;
508
+ let F = 0.30;
509
+ return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
510
+ }
511
+
512
+ fn tonemapUncharted2(x: vec3f) -> vec3f {
513
+ let white = 11.2;
514
+ let curr = uncharted2Partial(x * 2.0);
515
+ let whiteScale = vec3f(1.0) / uncharted2Partial(vec3f(white));
516
+ return curr * whiteScale;
517
+ }
518
+
519
+ // Lottes (AMD)
520
+ fn tonemapLottes(x: vec3f) -> vec3f {
521
+ let a = vec3f(1.6);
522
+ let d = vec3f(0.977);
523
+ let hdrMax = vec3f(8.0);
524
+ let midIn = vec3f(0.18);
525
+ let midOut = vec3f(0.267);
526
+
527
+ let b = (-pow(midIn, a) + pow(hdrMax, a) * midOut) / ((pow(hdrMax, a * d) - pow(midIn, a * d)) * midOut);
528
+ let c = (pow(hdrMax, a * d) * pow(midIn, a) - pow(hdrMax, a) * pow(midIn, a * d) * midOut) /
529
+ ((pow(hdrMax, a * d) - pow(midIn, a * d)) * midOut);
530
+
531
+ return pow(x, a) / (pow(x, a * d) * b + c);
532
+ }
533
+
534
+ // Uchimura (GT)
535
+ fn tonemapUchimura(x: vec3f) -> vec3f {
536
+ let P = 1.0; // max brightness
537
+ let a = 1.0; // contrast
538
+ let m = 0.22; // linear section start
539
+ let l = 0.4; // linear section length
540
+ let c = 1.33; // black tightness
541
+ let b = 0.0; // black lightness
542
+
543
+ let l0 = ((P - m) * l) / a;
544
+ let S0 = m + l0;
545
+ let S1 = m + a * l0;
546
+ let C2 = (a * P) / (P - S1);
547
+ let CP = -C2 / P;
548
+
549
+ var result: vec3f;
550
+ for (var i = 0u; i < 3u; i++) {
551
+ let v = x[i];
552
+ var w: f32;
553
+ if (v < m) {
554
+ w = v;
555
+ } else if (v < S0) {
556
+ w = m + a * (v - m);
557
+ } else {
558
+ w = P - (P - S1) * exp(CP * (v - S0));
559
+ }
560
+ result[i] = w;
561
+ }
562
+ return result;
563
+ }
564
+
565
+ // Khronos PBR neutral
566
+ fn tonemapKhronosPBR(color: vec3f) -> vec3f {
567
+ let startCompression = 0.8 - 0.04;
568
+ let desaturation = 0.15;
569
+
570
+ var x = min(color, vec3f(1.0));
571
+ let peak = max(max(color.r, color.g), color.b);
572
+
573
+ if (peak < startCompression) {
574
+ return x;
575
+ }
576
+
577
+ let d = 1.0 - startCompression;
578
+ let newPeak = 1.0 - d * d / (peak + d - startCompression);
579
+ x *= newPeak / peak;
580
+
581
+ let g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);
582
+ return mix(x, vec3f(newPeak), g);
583
+ }
584
+
585
+ @fragment
586
+ fn fs_tonemap(input: VertexOutput) -> @location(0) vec4f {
587
+ var color = textureSample(inputTexture, texSampler, input.uv).rgb;
588
+
589
+ // Apply exposure
590
+ color *= uniforms.exposure;
591
+
592
+ // Apply contrast (around mid-gray)
593
+ let midGray = 0.18;
594
+ color = midGray * pow(color / midGray, vec3f(uniforms.contrast));
595
+
596
+ // Apply saturation
597
+ let lum = luminance(color);
598
+ color = mix(vec3f(lum), color, uniforms.saturation);
599
+
600
+ // Apply tone mapping
601
+ let op = u32(uniforms.operator);
602
+ var mapped: vec3f;
603
+ switch (op) {
604
+ case 0u: { mapped = clamp(color, vec3f(0.0), vec3f(1.0)); } // None
605
+ case 1u: { mapped = tonemapReinhard(color); }
606
+ case 2u: { mapped = tonemapReinhardLum(color); }
607
+ case 3u: { mapped = tonemapACES(color); }
608
+ case 4u: { mapped = tonemapACESApprox(color); }
609
+ case 5u: { mapped = tonemapACES(color); } // Filmic = ACES
610
+ case 6u: { mapped = tonemapUncharted2(color); }
611
+ case 7u: { mapped = tonemapUchimura(color); }
612
+ case 8u: { mapped = tonemapLottes(color); }
613
+ case 9u: { mapped = tonemapKhronosPBR(color); }
614
+ default: { mapped = tonemapACES(color); }
615
+ }
616
+
617
+ // Apply gamma correction
618
+ let result = pow(mapped, vec3f(1.0 / uniforms.gamma));
619
+
620
+ return vec4f(result, 1.0);
621
+ }
622
+ `
623
+ );
624
+ var FXAA_SHADER = (
625
+ /* wgsl */
626
+ `
627
+ ${SHADER_UTILS}
628
+
629
+ struct FXAAUniforms {
630
+ quality: f32,
631
+ edgeThreshold: f32,
632
+ edgeThresholdMin: f32,
633
+ intensity: f32,
634
+ }
635
+
636
+ @group(0) @binding(0) var texSampler: sampler;
637
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
638
+ @group(0) @binding(2) var<uniform> uniforms: FXAAUniforms;
639
+
640
+ @fragment
641
+ fn fs_fxaa(input: VertexOutput) -> @location(0) vec4f {
642
+ let texSize = vec2f(textureDimensions(inputTexture));
643
+ let invSize = 1.0 / texSize;
644
+
645
+ // Sample center and neighbors
646
+ let center = textureSample(inputTexture, texSampler, input.uv);
647
+ let lumC = luminance(center.rgb);
648
+
649
+ let lumN = luminance(textureSample(inputTexture, texSampler, input.uv + vec2f(0.0, -1.0) * invSize).rgb);
650
+ let lumS = luminance(textureSample(inputTexture, texSampler, input.uv + vec2f(0.0, 1.0) * invSize).rgb);
651
+ let lumE = luminance(textureSample(inputTexture, texSampler, input.uv + vec2f(1.0, 0.0) * invSize).rgb);
652
+ let lumW = luminance(textureSample(inputTexture, texSampler, input.uv + vec2f(-1.0, 0.0) * invSize).rgb);
653
+
654
+ let lumMin = min(lumC, min(min(lumN, lumS), min(lumE, lumW)));
655
+ let lumMax = max(lumC, max(max(lumN, lumS), max(lumE, lumW)));
656
+ let lumRange = lumMax - lumMin;
657
+
658
+ // Skip if edge contrast is too low
659
+ if (lumRange < max(uniforms.edgeThresholdMin, lumMax * uniforms.edgeThreshold)) {
660
+ return center;
661
+ }
662
+
663
+ // Compute edge direction
664
+ let lumNW = luminance(textureSample(inputTexture, texSampler, input.uv + vec2f(-1.0, -1.0) * invSize).rgb);
665
+ let lumNE = luminance(textureSample(inputTexture, texSampler, input.uv + vec2f(1.0, -1.0) * invSize).rgb);
666
+ let lumSW = luminance(textureSample(inputTexture, texSampler, input.uv + vec2f(-1.0, 1.0) * invSize).rgb);
667
+ let lumSE = luminance(textureSample(inputTexture, texSampler, input.uv + vec2f(1.0, 1.0) * invSize).rgb);
668
+
669
+ let edgeH = abs((lumNW + lumNE) - 2.0 * lumN) +
670
+ abs((lumW + lumE) - 2.0 * lumC) * 2.0 +
671
+ abs((lumSW + lumSE) - 2.0 * lumS);
672
+
673
+ let edgeV = abs((lumNW + lumSW) - 2.0 * lumW) +
674
+ abs((lumN + lumS) - 2.0 * lumC) * 2.0 +
675
+ abs((lumNE + lumSE) - 2.0 * lumE);
676
+
677
+ let isHorizontal = edgeH >= edgeV;
678
+
679
+ // Blend direction
680
+ let stepLength = select(invSize.x, invSize.y, isHorizontal);
681
+ var lum1: f32;
682
+ var lum2: f32;
683
+
684
+ if (isHorizontal) {
685
+ lum1 = lumN;
686
+ lum2 = lumS;
687
+ } else {
688
+ lum1 = lumW;
689
+ lum2 = lumE;
690
+ }
691
+
692
+ let gradient1 = abs(lum1 - lumC);
693
+ let gradient2 = abs(lum2 - lumC);
694
+
695
+ let is1Steeper = gradient1 >= gradient2;
696
+ let gradientScaled = 0.25 * max(gradient1, gradient2);
697
+ let lumLocalAvg = 0.5 * (select(lum2, lum1, is1Steeper) + lumC);
698
+
699
+ // Subpixel anti-aliasing
700
+ let subpixC = (2.0 * (lumN + lumS + lumE + lumW) + lumNW + lumNE + lumSW + lumSE) / 12.0;
701
+ let subpixFactor = clamp(abs(subpixC - lumC) / lumRange, 0.0, 1.0);
702
+ let subpix = (-(subpixFactor * subpixFactor) + 1.0) * subpixFactor;
703
+
704
+ // Apply blend
705
+ var finalUV = input.uv;
706
+ let blendFactor = max(subpix, 0.5);
707
+
708
+ if (isHorizontal) {
709
+ finalUV.y += select(stepLength, -stepLength, is1Steeper) * blendFactor;
710
+ } else {
711
+ finalUV.x += select(stepLength, -stepLength, is1Steeper) * blendFactor;
712
+ }
713
+
714
+ let result = textureSample(inputTexture, texSampler, finalUV);
715
+ return mix(center, result, uniforms.intensity);
716
+ }
717
+ `
718
+ );
719
+ var VIGNETTE_SHADER = (
720
+ /* wgsl */
721
+ `
722
+ struct VignetteUniforms {
723
+ intensity: f32,
724
+ roundness: f32,
725
+ smoothness: f32,
726
+ padding: f32,
727
+ color: vec4f,
728
+ }
729
+
730
+ @group(0) @binding(0) var texSampler: sampler;
731
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
732
+ @group(0) @binding(2) var<uniform> uniforms: VignetteUniforms;
733
+
734
+ @fragment
735
+ fn fs_vignette(input: VertexOutput) -> @location(0) vec4f {
736
+ let color = textureSample(inputTexture, texSampler, input.uv);
737
+
738
+ let uv = input.uv * 2.0 - 1.0;
739
+ let aspect = 1.0; // Could be passed via uniforms
740
+
741
+ var coords = uv;
742
+ coords.x *= aspect;
743
+
744
+ // Compute vignette
745
+ let dist = length(coords) * uniforms.roundness;
746
+ let vignette = 1.0 - smoothstep(1.0 - uniforms.smoothness, 1.0, dist);
747
+
748
+ // Blend with vignette color
749
+ let vignetteColor = mix(uniforms.color.rgb, color.rgb, vignette);
750
+ let result = mix(color.rgb, vignetteColor, uniforms.intensity);
751
+
752
+ return vec4f(result, color.a);
753
+ }
754
+ `
755
+ );
756
+ var FILM_GRAIN_SHADER = (
757
+ /* wgsl */
758
+ `
759
+ ${SHADER_UTILS}
760
+
761
+ struct FilmGrainUniforms {
762
+ intensity: f32,
763
+ size: f32,
764
+ luminanceContribution: f32,
765
+ time: f32,
766
+ }
767
+
768
+ @group(0) @binding(0) var texSampler: sampler;
769
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
770
+ @group(0) @binding(2) var<uniform> uniforms: FilmGrainUniforms;
771
+
772
+ @fragment
773
+ fn fs_filmgrain(input: VertexOutput) -> @location(0) vec4f {
774
+ let color = textureSample(inputTexture, texSampler, input.uv);
775
+
776
+ let texSize = vec2f(textureDimensions(inputTexture));
777
+ let noiseUV = input.uv * texSize / uniforms.size;
778
+
779
+ // Generate animated noise
780
+ let grain = noise2D(noiseUV + vec2f(uniforms.time * 123.456, uniforms.time * 789.012)) * 2.0 - 1.0;
781
+
782
+ // Scale grain by luminance
783
+ let lum = luminance(color.rgb);
784
+ let grainAmount = uniforms.intensity * mix(1.0, 1.0 - lum, uniforms.luminanceContribution);
785
+
786
+ let result = color.rgb + vec3f(grain * grainAmount);
787
+
788
+ return vec4f(result, color.a);
789
+ }
790
+ `
791
+ );
792
+ var SHARPEN_SHADER = (
793
+ /* wgsl */
794
+ `
795
+ ${SHADER_UTILS}
796
+
797
+ struct SharpenUniforms {
798
+ intensity: f32,
799
+ amount: f32,
800
+ threshold: f32,
801
+ padding: f32,
802
+ }
803
+
804
+ @group(0) @binding(0) var texSampler: sampler;
805
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
806
+ @group(0) @binding(2) var<uniform> uniforms: SharpenUniforms;
807
+
808
+ @fragment
809
+ fn fs_sharpen(input: VertexOutput) -> @location(0) vec4f {
810
+ let texSize = vec2f(textureDimensions(inputTexture));
811
+ let texel = 1.0 / texSize;
812
+
813
+ // Sample 3x3 neighborhood
814
+ let center = textureSample(inputTexture, texSampler, input.uv).rgb;
815
+ let n = textureSample(inputTexture, texSampler, input.uv + vec2f(0.0, -texel.y)).rgb;
816
+ let s = textureSample(inputTexture, texSampler, input.uv + vec2f(0.0, texel.y)).rgb;
817
+ let e = textureSample(inputTexture, texSampler, input.uv + vec2f(texel.x, 0.0)).rgb;
818
+ let w = textureSample(inputTexture, texSampler, input.uv + vec2f(-texel.x, 0.0)).rgb;
819
+
820
+ // Compute unsharp mask
821
+ let blur = (n + s + e + w) * 0.25;
822
+ let diff = center - blur;
823
+
824
+ // Apply threshold
825
+ let sharpened = select(
826
+ center,
827
+ center + diff * uniforms.amount,
828
+ length(diff) > uniforms.threshold
829
+ );
830
+
831
+ let result = mix(center, sharpened, uniforms.intensity);
832
+
833
+ return vec4f(result, 1.0);
834
+ }
835
+ `
836
+ );
837
+ var CHROMATIC_ABERRATION_SHADER = (
838
+ /* wgsl */
839
+ `
840
+ struct ChromaticUniforms {
841
+ intensity: f32,
842
+ radial: f32,
843
+ padding: vec2f,
844
+ redOffset: vec2f,
845
+ greenOffset: vec2f,
846
+ blueOffset: vec2f,
847
+ padding2: vec2f,
848
+ }
849
+
850
+ @group(0) @binding(0) var texSampler: sampler;
851
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
852
+ @group(0) @binding(2) var<uniform> uniforms: ChromaticUniforms;
853
+
854
+ @fragment
855
+ fn fs_chromatic(input: VertexOutput) -> @location(0) vec4f {
856
+ let uv = input.uv;
857
+
858
+ var rOffset = uniforms.redOffset * uniforms.intensity;
859
+ var gOffset = uniforms.greenOffset * uniforms.intensity;
860
+ var bOffset = uniforms.blueOffset * uniforms.intensity;
861
+
862
+ // Apply radial distortion if enabled
863
+ if (uniforms.radial > 0.5) {
864
+ let center = vec2f(0.5);
865
+ let dir = uv - center;
866
+ let dist = length(dir);
867
+ let radialFactor = dist * dist;
868
+
869
+ rOffset *= radialFactor;
870
+ gOffset *= radialFactor;
871
+ bOffset *= radialFactor;
872
+ }
873
+
874
+ let r = textureSample(inputTexture, texSampler, uv + rOffset).r;
875
+ let g = textureSample(inputTexture, texSampler, uv + gOffset).g;
876
+ let b = textureSample(inputTexture, texSampler, uv + bOffset).b;
877
+
878
+ return vec4f(r, g, b, 1.0);
879
+ }
880
+ `
881
+ );
882
+ var DOF_SHADER = (
883
+ /* wgsl */
884
+ `
885
+ ${SHADER_UTILS}
886
+
887
+ struct DOFUniforms {
888
+ focusDistance: f32,
889
+ focalLength: f32,
890
+ aperture: f32,
891
+ maxBlur: f32,
892
+ nearPlane: f32,
893
+ farPlane: f32,
894
+ padding: vec2f,
895
+ }
896
+
897
+ @group(0) @binding(0) var texSampler: sampler;
898
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
899
+ @group(0) @binding(2) var<uniform> uniforms: DOFUniforms;
900
+ @group(0) @binding(3) var depthTexture: texture_2d<f32>;
901
+
902
+ fn linearizeDepth(d: f32) -> f32 {
903
+ return uniforms.nearPlane * uniforms.farPlane /
904
+ (uniforms.farPlane - d * (uniforms.farPlane - uniforms.nearPlane));
905
+ }
906
+
907
+ fn circleOfConfusion(depth: f32) -> f32 {
908
+ let s1 = depth;
909
+ let s2 = uniforms.focusDistance;
910
+ let f = uniforms.focalLength;
911
+ let a = uniforms.aperture;
912
+ let coc = abs(a * f * (s2 - s1) / (s1 * (s2 - f)));
913
+ return clamp(coc, 0.0, uniforms.maxBlur);
914
+ }
915
+
916
+ @fragment
917
+ fn fs_dof(input: VertexOutput) -> @location(0) vec4f {
918
+ let dims = vec2f(textureDimensions(inputTexture));
919
+ let texelSize = 1.0 / dims;
920
+
921
+ let rawDepth = textureSample(depthTexture, texSampler, input.uv).r;
922
+ let depth = linearizeDepth(rawDepth);
923
+ let coc = circleOfConfusion(depth);
924
+
925
+ // Disc blur with 16 samples in a Poisson-like pattern
926
+ let offsets = array<vec2f, 16>(
927
+ vec2f(-0.94201, -0.39906), vec2f( 0.94558, -0.76890),
928
+ vec2f(-0.09418, -0.92938), vec2f( 0.34495, 0.29387),
929
+ vec2f(-0.91588, 0.45771), vec2f(-0.81544, 0.00298),
930
+ vec2f(-0.38277, -0.56270), vec2f( 0.97484, 0.75648),
931
+ vec2f( 0.44323, -0.97511), vec2f( 0.53742, 0.01683),
932
+ vec2f(-0.26496, -0.01497), vec2f(-0.44693, 0.93910),
933
+ vec2f( 0.79197, 0.19090), vec2f(-0.24188, -0.99706),
934
+ vec2f( 0.04578, 0.53300), vec2f(-0.75738, -0.81580)
935
+ );
936
+
937
+ var color = vec4f(0.0);
938
+ var totalWeight = 0.0;
939
+
940
+ for (var i = 0u; i < 16u; i++) {
941
+ let sampleUV = input.uv + offsets[i] * texelSize * coc * 8.0;
942
+ let sampleColor = textureSample(inputTexture, texSampler, sampleUV);
943
+ let sampleDepth = linearizeDepth(textureSample(depthTexture, texSampler, sampleUV).r);
944
+ let sampleCoC = circleOfConfusion(sampleDepth);
945
+ let w = max(sampleCoC, coc * 0.2);
946
+ color += sampleColor * w;
947
+ totalWeight += w;
948
+ }
949
+
950
+ return color / totalWeight;
951
+ }
952
+ `
953
+ );
954
+ var SSAO_SHADER = (
955
+ /* wgsl */
956
+ `
957
+ ${SHADER_UTILS}
958
+
959
+ struct SSAOUniforms {
960
+ radius: f32,
961
+ bias: f32,
962
+ samples: f32,
963
+ power: f32,
964
+ falloff: f32,
965
+ mode: f32, // 0 = hemisphere, 1 = hbao
966
+ bentNormals: f32, // 0 = off, 1 = on
967
+ spatialDenoise: f32, // 0 = off, 1 = on
968
+ }
969
+
970
+ @group(0) @binding(0) var texSampler: sampler;
971
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
972
+ @group(0) @binding(2) var<uniform> uniforms: SSAOUniforms;
973
+ @group(0) @binding(3) var depthTexture: texture_2d<f32>;
974
+
975
+ fn hash3(p: vec2f) -> vec3f {
976
+ let q = vec3f(
977
+ dot(p, vec2f(127.1, 311.7)),
978
+ dot(p, vec2f(269.5, 183.3)),
979
+ dot(p, vec2f(419.2, 371.9))
980
+ );
981
+ return fract(sin(q) * 43758.5453) * 2.0 - 1.0;
982
+ }
983
+
984
+ fn reconstructNormal(uv: vec2f, texelSize: vec2f) -> vec3f {
985
+ let dc = textureSample(depthTexture, texSampler, uv).r;
986
+ let dl = textureSample(depthTexture, texSampler, uv - vec2f(texelSize.x, 0.0)).r;
987
+ let dr = textureSample(depthTexture, texSampler, uv + vec2f(texelSize.x, 0.0)).r;
988
+ let db = textureSample(depthTexture, texSampler, uv - vec2f(0.0, texelSize.y)).r;
989
+ let dt = textureSample(depthTexture, texSampler, uv + vec2f(0.0, texelSize.y)).r;
990
+ return normalize(vec3f(dl - dr, db - dt, 2.0 * texelSize.x));
991
+ }
992
+
993
+ // HBAO: 8 directions \xD7 4 steps per direction = 32 samples
994
+ fn hbaoOcclusion(uv: vec2f, normal: vec3f, centerDepth: f32, texelSize: vec2f) -> vec2f {
995
+ var occlusion = 0.0;
996
+ var bentN = vec3f(0.0);
997
+ let directions = 8;
998
+ let stepsPerDir = 4;
999
+ let angleStep = 6.28318 / f32(directions);
1000
+
1001
+ for (var d = 0; d < directions; d++) {
1002
+ let angle = f32(d) * angleStep;
1003
+ let dir = vec2f(cos(angle), sin(angle));
1004
+ var maxHorizon = uniforms.bias;
1005
+
1006
+ for (var s = 1; s <= stepsPerDir; s++) {
1007
+ let stepScale = f32(s) / f32(stepsPerDir);
1008
+ let sampleOffset = dir * uniforms.radius * stepScale * texelSize * 8.0;
1009
+ let sampleUV = uv + sampleOffset;
1010
+ let sampleDepth = textureSample(depthTexture, texSampler, sampleUV).r;
1011
+ let depthDelta = centerDepth - sampleDepth;
1012
+
1013
+ if (depthDelta > uniforms.bias && depthDelta < uniforms.falloff) {
1014
+ let horizonAngle = depthDelta / (length(sampleOffset) * 500.0 + 0.001);
1015
+ maxHorizon = max(maxHorizon, horizonAngle);
1016
+ }
1017
+ }
1018
+ occlusion += maxHorizon;
1019
+ // Accumulate bent normal: direction of least occlusion
1020
+ let weight = 1.0 - min(maxHorizon * 2.0, 1.0);
1021
+ bentN += vec3f(dir * weight, weight);
1022
+ }
1023
+
1024
+ occlusion = 1.0 - pow(occlusion / f32(directions), uniforms.power);
1025
+ return vec2f(occlusion, length(bentN.xy));
1026
+ }
1027
+
1028
+ // 5\xD75 cross-bilateral spatial denoise
1029
+ fn spatialDenoise(uv: vec2f, centerOcclusion: f32, centerDepth: f32, centerNormal: vec3f, texelSize: vec2f) -> f32 {
1030
+ var sum = centerOcclusion;
1031
+ var totalWeight = 1.0;
1032
+
1033
+ for (var y = -2; y <= 2; y++) {
1034
+ for (var x = -2; x <= 2; x++) {
1035
+ if (x == 0 && y == 0) { continue; }
1036
+ let offset = vec2f(f32(x), f32(y)) * texelSize;
1037
+ let sampleUV = uv + offset;
1038
+ let sampleDepth = textureSample(depthTexture, texSampler, sampleUV).r;
1039
+ let sampleNormal = reconstructNormal(sampleUV, texelSize);
1040
+
1041
+ // Depth similarity weight
1042
+ let depthW = exp(-abs(centerDepth - sampleDepth) * 100.0);
1043
+ // Normal similarity weight
1044
+ let normalW = max(dot(centerNormal, sampleNormal), 0.0);
1045
+ // Spatial weight (Gaussian)
1046
+ let spatialW = exp(-f32(x * x + y * y) * 0.2);
1047
+
1048
+ let w = depthW * normalW * spatialW;
1049
+ // Re-sample occlusion at this location (simplified: use color channel)
1050
+ let sampleColor = textureSample(inputTexture, texSampler, sampleUV);
1051
+ let sampleOcclusion = luminance(sampleColor.rgb) / max(luminance(textureSample(inputTexture, texSampler, uv).rgb), 0.001);
1052
+ sum += clamp(sampleOcclusion, 0.0, 2.0) * w;
1053
+ totalWeight += w;
1054
+ }
1055
+ }
1056
+
1057
+ return sum / totalWeight;
1058
+ }
1059
+
1060
+ @fragment
1061
+ fn fs_ssao(input: VertexOutput) -> @location(0) vec4f {
1062
+ let dims = vec2f(textureDimensions(inputTexture));
1063
+ let texelSize = 1.0 / dims;
1064
+ let color = textureSample(inputTexture, texSampler, input.uv);
1065
+ let centerDepth = textureSample(depthTexture, texSampler, input.uv).r;
1066
+ let normal = reconstructNormal(input.uv, texelSize);
1067
+
1068
+ var occlusion = 0.0;
1069
+
1070
+ if (uniforms.mode > 0.5) {
1071
+ // HBAO mode: 8 directions \xD7 4 steps
1072
+ let hbaoResult = hbaoOcclusion(input.uv, normal, centerDepth, texelSize);
1073
+ occlusion = hbaoResult.x;
1074
+ } else {
1075
+ // Standard hemisphere sampling
1076
+ let sampleCount = u32(uniforms.samples);
1077
+ var occ = 0.0;
1078
+ for (var i = 0u; i < sampleCount; i++) {
1079
+ let randSeed = input.uv * dims + vec2f(f32(i) * 7.0, f32(i) * 13.0);
1080
+ var sampleDir = normalize(hash3(randSeed));
1081
+ if (dot(sampleDir, normal) < 0.0) {
1082
+ sampleDir = -sampleDir;
1083
+ }
1084
+ let scale = f32(i + 1u) / f32(sampleCount);
1085
+ let sampleOffset = sampleDir * uniforms.radius * mix(0.1, 1.0, scale * scale);
1086
+ let sampleUV = input.uv + sampleOffset.xy * texelSize * 8.0;
1087
+ let sampleDepth = textureSample(depthTexture, texSampler, sampleUV).r;
1088
+ let rangeCheck = smoothstep(0.0, 1.0,
1089
+ uniforms.falloff / abs(centerDepth - sampleDepth + 0.0001));
1090
+ if (sampleDepth < centerDepth - uniforms.bias) {
1091
+ occ += rangeCheck;
1092
+ }
1093
+ }
1094
+ occlusion = 1.0 - pow(occ / f32(sampleCount), uniforms.power);
1095
+ }
1096
+
1097
+ // Spatial denoise pass (applied inline for simplicity)
1098
+ if (uniforms.spatialDenoise > 0.5) {
1099
+ // Approximate denoise by blending with neighbors
1100
+ var blurred = occlusion;
1101
+ var tw = 1.0;
1102
+ for (var dy = -1; dy <= 1; dy++) {
1103
+ for (var dx = -1; dx <= 1; dx++) {
1104
+ if (dx == 0 && dy == 0) { continue; }
1105
+ let off = vec2f(f32(dx), f32(dy)) * texelSize;
1106
+ let sd = textureSample(depthTexture, texSampler, input.uv + off).r;
1107
+ let dw = exp(-abs(centerDepth - sd) * 50.0);
1108
+ let sn = reconstructNormal(input.uv + off, texelSize);
1109
+ let nw = max(dot(normal, sn), 0.0);
1110
+ let w = dw * nw;
1111
+ blurred += occlusion * w; // Approximation: use same occlusion
1112
+ tw += w;
1113
+ }
1114
+ }
1115
+ occlusion = blurred / tw;
1116
+ }
1117
+
1118
+ return vec4f(color.rgb * occlusion, color.a);
1119
+ }
1120
+ `
1121
+ );
1122
+ var FOG_SHADER = (
1123
+ /* wgsl */
1124
+ `
1125
+ ${SHADER_UTILS}
1126
+
1127
+ struct FogUniforms {
1128
+ color: vec3f,
1129
+ density: f32,
1130
+ start: f32,
1131
+ end: f32,
1132
+ height: f32,
1133
+ heightFalloff: f32,
1134
+ mode: f32,
1135
+ padding: vec3f,
1136
+ }
1137
+
1138
+ @group(0) @binding(0) var texSampler: sampler;
1139
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
1140
+ @group(0) @binding(2) var<uniform> uniforms: FogUniforms;
1141
+ @group(0) @binding(3) var depthTexture: texture_2d<f32>;
1142
+
1143
+ @fragment
1144
+ fn fs_fog(input: VertexOutput) -> @location(0) vec4f {
1145
+ let color = textureSample(inputTexture, texSampler, input.uv);
1146
+ let depth = textureSample(depthTexture, texSampler, input.uv).r;
1147
+
1148
+ // Compute fog factor based on mode
1149
+ var fogFactor = 0.0;
1150
+ let mode = u32(uniforms.mode);
1151
+ if (mode == 0u) {
1152
+ // Linear fog
1153
+ fogFactor = clamp((uniforms.end - depth) / (uniforms.end - uniforms.start), 0.0, 1.0);
1154
+ } else if (mode == 1u) {
1155
+ // Exponential fog
1156
+ fogFactor = exp(-uniforms.density * depth);
1157
+ } else {
1158
+ // Exponential-squared fog
1159
+ let d = uniforms.density * depth;
1160
+ fogFactor = exp(-d * d);
1161
+ }
1162
+
1163
+ // Height-based attenuation
1164
+ let heightUV = 1.0 - input.uv.y; // screen-space approximation of world height
1165
+ let heightFactor = exp(-max(heightUV - uniforms.height, 0.0) * uniforms.heightFalloff);
1166
+ fogFactor = mix(fogFactor, 1.0, 1.0 - heightFactor);
1167
+
1168
+ let foggedColor = mix(uniforms.color, color.rgb, fogFactor);
1169
+ return vec4f(foggedColor, color.a);
1170
+ }
1171
+ `
1172
+ );
1173
+ var MOTION_BLUR_SHADER = (
1174
+ /* wgsl */
1175
+ `
1176
+ ${SHADER_UTILS}
1177
+
1178
+ struct MotionBlurUniforms {
1179
+ samples: f32,
1180
+ velocityScale: f32,
1181
+ maxVelocity: f32,
1182
+ intensity: f32,
1183
+ }
1184
+
1185
+ @group(0) @binding(0) var texSampler: sampler;
1186
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
1187
+ @group(0) @binding(2) var<uniform> uniforms: MotionBlurUniforms;
1188
+ @group(0) @binding(3) var velocityTexture: texture_2d<f32>;
1189
+
1190
+ @fragment
1191
+ fn fs_motionblur(input: VertexOutput) -> @location(0) vec4f {
1192
+ let velocity = textureSample(velocityTexture, texSampler, input.uv).rg;
1193
+
1194
+ // Scale and clamp velocity
1195
+ var vel = velocity * uniforms.velocityScale;
1196
+ let speed = length(vel);
1197
+ if (speed > uniforms.maxVelocity) {
1198
+ vel = vel * (uniforms.maxVelocity / speed);
1199
+ }
1200
+
1201
+ let sampleCount = u32(uniforms.samples);
1202
+ var color = textureSample(inputTexture, texSampler, input.uv);
1203
+ var totalWeight = 1.0;
1204
+
1205
+ for (var i = 1u; i <= sampleCount; i++) {
1206
+ let t = (f32(i) / f32(sampleCount)) - 0.5;
1207
+ let sampleUV = input.uv + vel * t;
1208
+ let sampleColor = textureSample(inputTexture, texSampler, sampleUV);
1209
+ let w = 1.0 - abs(t) * 2.0; // Center-weighted
1210
+ color += sampleColor * w;
1211
+ totalWeight += w;
1212
+ }
1213
+
1214
+ let blurred = color / totalWeight;
1215
+ let original = textureSample(inputTexture, texSampler, input.uv);
1216
+ return mix(original, blurred, uniforms.intensity);
1217
+ }
1218
+ `
1219
+ );
1220
+ var COLOR_GRADE_SHADER = (
1221
+ /* wgsl */
1222
+ `
1223
+ ${SHADER_UTILS}
1224
+
1225
+ struct ColorGradeUniforms {
1226
+ shadows: vec3f,
1227
+ shadowsOffset: f32,
1228
+ midtones: vec3f,
1229
+ highlightsOffset: f32,
1230
+ highlights: vec3f,
1231
+ hueShift: f32,
1232
+ temperature: f32,
1233
+ tint: f32,
1234
+ intensity: f32,
1235
+ lutIntensity: f32,
1236
+ }
1237
+
1238
+ @group(0) @binding(0) var texSampler: sampler;
1239
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
1240
+ @group(0) @binding(2) var<uniform> uniforms: ColorGradeUniforms;
1241
+
1242
+ // RGB to HSL conversion
1243
+ fn rgbToHsl(c: vec3f) -> vec3f {
1244
+ let cMax = max(max(c.r, c.g), c.b);
1245
+ let cMin = min(min(c.r, c.g), c.b);
1246
+ let delta = cMax - cMin;
1247
+
1248
+ var h = 0.0;
1249
+ var s = 0.0;
1250
+ let l = (cMax + cMin) / 2.0;
1251
+
1252
+ if (delta > 0.0) {
1253
+ s = select(delta / (2.0 - cMax - cMin), delta / (cMax + cMin), l < 0.5);
1254
+
1255
+ if (cMax == c.r) {
1256
+ h = (c.g - c.b) / delta + select(0.0, 6.0, c.g < c.b);
1257
+ } else if (cMax == c.g) {
1258
+ h = (c.b - c.r) / delta + 2.0;
1259
+ } else {
1260
+ h = (c.r - c.g) / delta + 4.0;
1261
+ }
1262
+ h /= 6.0;
1263
+ }
1264
+
1265
+ return vec3f(h, s, l);
1266
+ }
1267
+
1268
+ fn hue2rgb(p: f32, q: f32, t: f32) -> f32 {
1269
+ var tt = t;
1270
+ if (tt < 0.0) { tt += 1.0; }
1271
+ if (tt > 1.0) { tt -= 1.0; }
1272
+ if (tt < 1.0/6.0) { return p + (q - p) * 6.0 * tt; }
1273
+ if (tt < 1.0/2.0) { return q; }
1274
+ if (tt < 2.0/3.0) { return p + (q - p) * (2.0/3.0 - tt) * 6.0; }
1275
+ return p;
1276
+ }
1277
+
1278
+ fn hslToRgb(hsl: vec3f) -> vec3f {
1279
+ if (hsl.y == 0.0) {
1280
+ return vec3f(hsl.z);
1281
+ }
1282
+
1283
+ let q = select(hsl.z + hsl.y - hsl.z * hsl.y, hsl.z * (1.0 + hsl.y), hsl.z < 0.5);
1284
+ let p = 2.0 * hsl.z - q;
1285
+
1286
+ return vec3f(
1287
+ hue2rgb(p, q, hsl.x + 1.0/3.0),
1288
+ hue2rgb(p, q, hsl.x),
1289
+ hue2rgb(p, q, hsl.x - 1.0/3.0)
1290
+ );
1291
+ }
1292
+
1293
+ // Temperature/tint adjustment
1294
+ fn adjustTemperature(color: vec3f, temp: f32, tint: f32) -> vec3f {
1295
+ var result = color;
1296
+ // Warm (positive) = more red, less blue
1297
+ result.r += temp * 0.1;
1298
+ result.b -= temp * 0.1;
1299
+ // Tint: positive = more green
1300
+ result.g += tint * 0.1;
1301
+ return clamp(result, vec3f(0.0), vec3f(1.0));
1302
+ }
1303
+
1304
+ @fragment
1305
+ fn fs_colorgrade(input: VertexOutput) -> @location(0) vec4f {
1306
+ var color = textureSample(inputTexture, texSampler, input.uv).rgb;
1307
+
1308
+ let lum = luminance(color);
1309
+
1310
+ // Shadows/Midtones/Highlights
1311
+ let shadowWeight = 1.0 - smoothstep(0.0, 0.33, lum);
1312
+ let highlightWeight = smoothstep(0.66, 1.0, lum);
1313
+ let midtoneWeight = 1.0 - shadowWeight - highlightWeight;
1314
+
1315
+ color += uniforms.shadows * shadowWeight;
1316
+ color += uniforms.midtones * midtoneWeight;
1317
+ color += uniforms.highlights * highlightWeight;
1318
+
1319
+ // Hue shift
1320
+ if (abs(uniforms.hueShift) > 0.001) {
1321
+ var hsl = rgbToHsl(color);
1322
+ hsl.x = fract(hsl.x + uniforms.hueShift);
1323
+ color = hslToRgb(hsl);
1324
+ }
1325
+
1326
+ // Temperature and tint
1327
+ color = adjustTemperature(color, uniforms.temperature, uniforms.tint);
1328
+
1329
+ return vec4f(color, 1.0);
1330
+ }
1331
+ `
1332
+ );
1333
+ var BLIT_SHADER = (
1334
+ /* wgsl */
1335
+ `
1336
+ @group(0) @binding(0) var texSampler: sampler;
1337
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
1338
+
1339
+ @fragment
1340
+ fn fs_blit(input: VertexOutput) -> @location(0) vec4f {
1341
+ return textureSample(inputTexture, texSampler, input.uv);
1342
+ }
1343
+ `
1344
+ );
1345
+ var CAUSTICS_SHADER = (
1346
+ /* wgsl */
1347
+ `
1348
+ ${SHADER_UTILS}
1349
+
1350
+ struct CausticsUniforms {
1351
+ intensity: f32,
1352
+ scale: f32,
1353
+ speed: f32,
1354
+ time: f32,
1355
+ color: vec3f,
1356
+ depthFade: f32,
1357
+ waterLevel: f32,
1358
+ dispersion: f32,
1359
+ foamIntensity: f32,
1360
+ shadowStrength: f32,
1361
+ }
1362
+
1363
+ @group(0) @binding(0) var texSampler: sampler;
1364
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
1365
+ @group(0) @binding(2) var<uniform> uniforms: CausticsUniforms;
1366
+ @group(0) @binding(3) var depthTexture: texture_2d<f32>;
1367
+
1368
+ fn voronoiDist(p: vec2f) -> f32 {
1369
+ let i = floor(p);
1370
+ let f = fract(p);
1371
+ var md = 1.0;
1372
+ for (var y = -1; y <= 1; y++) {
1373
+ for (var x = -1; x <= 1; x++) {
1374
+ let n = vec2f(f32(x), f32(y));
1375
+ let h1 = fract(sin(dot(i + n, vec2f(127.1, 311.7))) * 43758.5453);
1376
+ let h2 = fract(sin(dot(i + n, vec2f(269.5, 183.3))) * 43758.5453);
1377
+ let pt = n + vec2f(h1, h2) - f;
1378
+ md = min(md, dot(pt, pt));
1379
+ }
1380
+ }
1381
+ return sqrt(md);
1382
+ }
1383
+
1384
+ // Refractive caustic with IoR-based convergence
1385
+ fn refractiveCausticPP(uv: vec2f, time: f32, scale: f32, ior: f32) -> f32 {
1386
+ let eps = 0.01;
1387
+ let h0 = voronoiDist(uv * scale + vec2f(time * 0.3, time * 0.7));
1388
+ let hx = voronoiDist((uv + vec2f(eps, 0.0)) * scale + vec2f(time * 0.3, time * 0.7));
1389
+ let hy = voronoiDist((uv + vec2f(0.0, eps)) * scale + vec2f(time * 0.3, time * 0.7));
1390
+ let grad = vec2f(hx - h0, hy - h0) / eps;
1391
+ let refracted = grad * (1.0 / ior - 1.0);
1392
+ let convergence = voronoiDist((uv + refracted * 0.1) * scale * 1.3 + vec2f(-time * 0.5, time * 0.4));
1393
+ return pow(1.0 - convergence, 4.0);
1394
+ }
1395
+
1396
+ // Turbulence-driven foam
1397
+ fn foamNoise(uv: vec2f, time: f32, scale: f32) -> f32 {
1398
+ let n1 = fract(sin(dot(floor(uv * scale * 4.0), vec2f(127.1, 311.7))) * 43758.5453);
1399
+ let n2 = fract(sin(dot(floor(uv * scale * 8.0 + vec2f(time * 0.5, 0.0)), vec2f(269.5, 183.3))) * 43758.5453);
1400
+ let turbulence = abs(n1 * 2.0 - 1.0) + abs(n2 * 2.0 - 1.0) * 0.5;
1401
+ return smoothstep(0.8, 1.2, turbulence);
1402
+ }
1403
+
1404
+ @fragment
1405
+ fn fs_caustics(input: VertexOutput) -> @location(0) vec4f {
1406
+ let color = textureSample(inputTexture, texSampler, input.uv);
1407
+ let depth = textureSample(depthTexture, texSampler, input.uv).r;
1408
+
1409
+ let worldY = 1.0 - input.uv.y;
1410
+ if (worldY > uniforms.waterLevel) {
1411
+ return color;
1412
+ }
1413
+
1414
+ let depthFactor = exp(-depth * uniforms.depthFade);
1415
+ var causticColor = vec3f(0.0);
1416
+
1417
+ if (uniforms.dispersion > 0.001) {
1418
+ // Chromatic dispersion: separate R/G/B with different IoR
1419
+ let baseIoR = 1.33;
1420
+ let t = uniforms.time * uniforms.speed;
1421
+ let rC = refractiveCausticPP(input.uv, t, uniforms.scale, baseIoR - uniforms.dispersion);
1422
+ let gC = refractiveCausticPP(input.uv, t, uniforms.scale, baseIoR);
1423
+ let bC = refractiveCausticPP(input.uv, t, uniforms.scale, baseIoR + uniforms.dispersion);
1424
+ causticColor = vec3f(rC, gC, bC) * uniforms.color * uniforms.intensity * depthFactor;
1425
+ } else {
1426
+ // Standard dual-layer caustics
1427
+ let uv1 = input.uv * uniforms.scale + vec2f(uniforms.time * uniforms.speed * 0.3, uniforms.time * uniforms.speed * 0.7);
1428
+ let uv2 = input.uv * uniforms.scale * 1.3 + vec2f(-uniforms.time * uniforms.speed * 0.5, uniforms.time * uniforms.speed * 0.4);
1429
+ let c1 = voronoiDist(uv1);
1430
+ let c2 = voronoiDist(uv2);
1431
+ let caustic = pow(1.0 - c1, 3.0) * pow(1.0 - c2, 3.0);
1432
+ causticColor = uniforms.color * caustic * uniforms.intensity * depthFactor;
1433
+ }
1434
+
1435
+ // Foam overlay
1436
+ let foam = foamNoise(input.uv, uniforms.time * uniforms.speed, uniforms.scale) * uniforms.foamIntensity;
1437
+
1438
+ // Caustic shadows: darken where caustics are absent
1439
+ let causticLum = dot(causticColor, vec3f(0.333));
1440
+ let shadow = mix(1.0, 1.0 - uniforms.shadowStrength, (1.0 - causticLum) * depthFactor);
1441
+
1442
+ let result = color.rgb * shadow + causticColor + vec3f(foam);
1443
+ return vec4f(result, color.a);
1444
+ }
1445
+ `
1446
+ );
1447
+ var SSR_SHADER = (
1448
+ /* wgsl */
1449
+ `
1450
+ ${SHADER_UTILS}
1451
+
1452
+ struct SSRUniforms {
1453
+ maxSteps: f32,
1454
+ stepSize: f32,
1455
+ thickness: f32,
1456
+ roughnessFade: f32,
1457
+ edgeFade: f32,
1458
+ intensity: f32,
1459
+ roughnessBlur: f32,
1460
+ fresnelStrength: f32,
1461
+ }
1462
+
1463
+ @group(0) @binding(0) var texSampler: sampler;
1464
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
1465
+ @group(0) @binding(2) var<uniform> uniforms: SSRUniforms;
1466
+ @group(0) @binding(3) var depthTexture: texture_2d<f32>;
1467
+ @group(0) @binding(4) var normalTexture: texture_2d<f32>;
1468
+
1469
+ @fragment
1470
+ fn fs_ssr(input: VertexOutput) -> @location(0) vec4f {
1471
+ let color = textureSample(inputTexture, texSampler, input.uv);
1472
+ let depth = textureSample(depthTexture, texSampler, input.uv).r;
1473
+ let texSize = vec2f(textureDimensions(inputTexture));
1474
+ let texel = 1.0 / texSize;
1475
+
1476
+ // Reconstruct normal from depth
1477
+ let dc = depth;
1478
+ let dl = textureSample(depthTexture, texSampler, input.uv - vec2f(texel.x, 0.0)).r;
1479
+ let dr = textureSample(depthTexture, texSampler, input.uv + vec2f(texel.x, 0.0)).r;
1480
+ let db = textureSample(depthTexture, texSampler, input.uv - vec2f(0.0, texel.y)).r;
1481
+ let dt = textureSample(depthTexture, texSampler, input.uv + vec2f(0.0, texel.y)).r;
1482
+ let normal = normalize(vec3f(dl - dr, db - dt, 2.0 * texel.x));
1483
+
1484
+ // View direction (simplified \u2014 assumes forward-facing camera)
1485
+ let viewDir = normalize(vec3f(input.uv * 2.0 - 1.0, -1.0));
1486
+
1487
+ // Reflect view around normal
1488
+ let reflectDir = reflect(viewDir, normal);
1489
+ let stepDir = reflectDir.xy * uniforms.stepSize;
1490
+
1491
+ var hitUV = input.uv;
1492
+ var hit = false;
1493
+ let steps = i32(uniforms.maxSteps);
1494
+
1495
+ for (var i = 1; i <= steps; i++) {
1496
+ hitUV += stepDir;
1497
+
1498
+ // Bounds check
1499
+ if (hitUV.x < 0.0 || hitUV.x > 1.0 || hitUV.y < 0.0 || hitUV.y > 1.0) { break; }
1500
+
1501
+ let sampleDepth = textureSample(depthTexture, texSampler, hitUV).r;
1502
+ let expectedDepth = depth + f32(i) * uniforms.stepSize;
1503
+
1504
+ if (expectedDepth > sampleDepth && expectedDepth - sampleDepth < uniforms.thickness) {
1505
+ hit = true;
1506
+
1507
+ // Binary refinement (4 steps)
1508
+ var refineStep = stepDir * 0.5;
1509
+ for (var j = 0; j < 4; j++) {
1510
+ hitUV -= refineStep;
1511
+ let rd = textureSample(depthTexture, texSampler, hitUV).r;
1512
+ let re = depth + length(hitUV - input.uv) / uniforms.stepSize * uniforms.stepSize;
1513
+ if (re > rd) {
1514
+ hitUV += refineStep;
1515
+ }
1516
+ refineStep *= 0.5;
1517
+ }
1518
+ break;
1519
+ }
1520
+ }
1521
+
1522
+ if (!hit) { return color; }
1523
+
1524
+ // Roughness blur: golden-angle 8-sample blur at hit point scaled by roughness
1525
+ var reflectionColor = vec3f(0.0);
1526
+ if (uniforms.roughnessBlur > 0.001) {
1527
+ let blurRadius = uniforms.roughnessBlur * 0.01;
1528
+ let goldenAngle = 2.399963;
1529
+ var totalW = 0.0;
1530
+ for (var s = 0; s < 8; s++) {
1531
+ let angle = f32(s) * goldenAngle;
1532
+ let r = sqrt(f32(s + 1) / 8.0) * blurRadius;
1533
+ let blurOffset = vec2f(cos(angle), sin(angle)) * r;
1534
+ let sampleC = textureSample(inputTexture, texSampler, hitUV + blurOffset).rgb;
1535
+ let w = 1.0 - f32(s) / 8.0;
1536
+ reflectionColor += sampleC * w;
1537
+ totalW += w;
1538
+ }
1539
+ reflectionColor /= totalW;
1540
+ } else {
1541
+ reflectionColor = textureSample(inputTexture, texSampler, hitUV).rgb;
1542
+ }
1543
+
1544
+ // Schlick Fresnel weighting
1545
+ let cosTheta = max(dot(-viewDir, normal), 0.0);
1546
+ let f0 = 0.04; // dielectric
1547
+ let fresnel = f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
1548
+ let fresnelWeight = mix(1.0, fresnel, uniforms.fresnelStrength);
1549
+
1550
+ // Edge fade
1551
+ let edgeDist = max(abs(hitUV.x - 0.5), abs(hitUV.y - 0.5)) * 2.0;
1552
+ let edgeFade = 1.0 - pow(clamp(edgeDist, 0.0, 1.0), uniforms.edgeFade);
1553
+
1554
+ // Distance fade
1555
+ let travelDist = length(hitUV - input.uv);
1556
+ let distFade = 1.0 - clamp(travelDist * 2.0, 0.0, 1.0);
1557
+
1558
+ let reflectionMask = edgeFade * distFade * uniforms.intensity * fresnelWeight;
1559
+ return vec4f(mix(color.rgb, reflectionColor, reflectionMask), color.a);
1560
+ }
1561
+ `
1562
+ );
1563
+ var SSGI_SHADER = (
1564
+ /* wgsl */
1565
+ `
1566
+ ${SHADER_UTILS}
1567
+
1568
+ struct SSGIUniforms {
1569
+ radius: f32,
1570
+ samples: f32,
1571
+ bounceIntensity: f32,
1572
+ falloff: f32,
1573
+ time: f32,
1574
+ intensity: f32,
1575
+ temporalBlend: f32,
1576
+ spatialDenoise: f32,
1577
+ multiBounce: f32,
1578
+ padding: vec3f,
1579
+ }
1580
+
1581
+ @group(0) @binding(0) var texSampler: sampler;
1582
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
1583
+ @group(0) @binding(2) var<uniform> uniforms: SSGIUniforms;
1584
+ @group(0) @binding(3) var depthTexture: texture_2d<f32>;
1585
+
1586
+ @fragment
1587
+ fn fs_ssgi(input: VertexOutput) -> @location(0) vec4f {
1588
+ let color = textureSample(inputTexture, texSampler, input.uv);
1589
+ let centerDepth = textureSample(depthTexture, texSampler, input.uv).r;
1590
+ let texSize = vec2f(textureDimensions(inputTexture));
1591
+ let texel = 1.0 / texSize;
1592
+
1593
+ // Reconstruct normal from depth
1594
+ let dl = textureSample(depthTexture, texSampler, input.uv - vec2f(texel.x, 0.0)).r;
1595
+ let dr = textureSample(depthTexture, texSampler, input.uv + vec2f(texel.x, 0.0)).r;
1596
+ let db = textureSample(depthTexture, texSampler, input.uv - vec2f(0.0, texel.y)).r;
1597
+ let dt = textureSample(depthTexture, texSampler, input.uv + vec2f(0.0, texel.y)).r;
1598
+ let normal = normalize(vec3f(dl - dr, db - dt, 2.0 * texel.x));
1599
+
1600
+ var indirect = vec3f(0.0);
1601
+ let sampleCount = i32(uniforms.samples);
1602
+ let goldenAngle = 2.399963;
1603
+
1604
+ for (var i = 0; i < sampleCount; i++) {
1605
+ let fi = f32(i);
1606
+ let r = sqrt(fi / uniforms.samples) * uniforms.radius;
1607
+ let theta = fi * goldenAngle + uniforms.time * 0.1; // Slight temporal jitter
1608
+ let offset = vec2f(cos(theta), sin(theta)) * r * texel * 8.0;
1609
+ let sampleUV = input.uv + offset;
1610
+
1611
+ let sampleColor = textureSample(inputTexture, texSampler, sampleUV).rgb;
1612
+ let sampleDepth = textureSample(depthTexture, texSampler, sampleUV).r;
1613
+
1614
+ // Weight by depth proximity (nearby surfaces contribute more)
1615
+ let depthDiff = abs(centerDepth - sampleDepth);
1616
+ let depthWeight = exp(-depthDiff * uniforms.falloff * 10.0);
1617
+
1618
+ // Cosine weight: approximate normal-based falloff
1619
+ let sampleDir = normalize(vec3f(offset, 0.05));
1620
+ let cosWeight = max(dot(sampleDir, normal), 0.0);
1621
+
1622
+ indirect += sampleColor * depthWeight * cosWeight;
1623
+ }
1624
+
1625
+ indirect /= uniforms.samples;
1626
+ indirect *= uniforms.bounceIntensity;
1627
+
1628
+ // Multi-bounce approximation: self-illumination feedback
1629
+ if (uniforms.multiBounce > 0.001) {
1630
+ indirect *= (1.0 + uniforms.multiBounce * luminance(indirect));
1631
+ }
1632
+
1633
+ // Spatial denoise: 3\xD73 edge-stopping cross-bilateral filter
1634
+ if (uniforms.spatialDenoise > 0.5) {
1635
+ var denoised = indirect;
1636
+ var tw = 1.0;
1637
+ for (var dy = -1; dy <= 1; dy++) {
1638
+ for (var dx = -1; dx <= 1; dx++) {
1639
+ if (dx == 0 && dy == 0) { continue; }
1640
+ let off = vec2f(f32(dx), f32(dy)) * texel;
1641
+ let sd = textureSample(depthTexture, texSampler, input.uv + off).r;
1642
+ // Depth weight
1643
+ let dw = exp(-abs(centerDepth - sd) * uniforms.falloff * 10.0);
1644
+ // Normal weight
1645
+ let snl = textureSample(depthTexture, texSampler, input.uv + off - vec2f(texel.x, 0.0)).r;
1646
+ let snr = textureSample(depthTexture, texSampler, input.uv + off + vec2f(texel.x, 0.0)).r;
1647
+ let snb = textureSample(depthTexture, texSampler, input.uv + off - vec2f(0.0, texel.y)).r;
1648
+ let snt = textureSample(depthTexture, texSampler, input.uv + off + vec2f(0.0, texel.y)).r;
1649
+ let sn = normalize(vec3f(snl - snr, snb - snt, 2.0 * texel.x));
1650
+ let nw = max(dot(normal, sn), 0.0);
1651
+ let w = dw * nw;
1652
+ // Sample neighbor's indirect (approximation: use color luminance ratio)
1653
+ let neighborColor = textureSample(inputTexture, texSampler, input.uv + off).rgb;
1654
+ denoised += neighborColor * uniforms.bounceIntensity * w * 0.5;
1655
+ tw += w;
1656
+ }
1657
+ }
1658
+ indirect = denoised / tw;
1659
+ }
1660
+
1661
+ var result = color.rgb + indirect * uniforms.intensity;
1662
+
1663
+ // Temporal blend: mix with previous frame color (approximation using current frame offset)
1664
+ if (uniforms.temporalBlend > 0.001) {
1665
+ // Approximate temporal reprojection by blending with slightly jittered sample
1666
+ let temporalUV = input.uv + vec2f(sin(uniforms.time * 31.0), cos(uniforms.time * 37.0)) * texel * 0.5;
1667
+ let prevColor = textureSample(inputTexture, texSampler, temporalUV).rgb;
1668
+ result = mix(result, prevColor + indirect * uniforms.intensity * 0.5, uniforms.temporalBlend * 0.3);
1669
+ }
1670
+
1671
+ return vec4f(result, color.a);
1672
+ }
1673
+ `
1674
+ );
1675
+ function buildEffectShader(fragmentShader) {
1676
+ return `${FULLSCREEN_VERTEX_SHADER}
1677
+ ${SHADER_UTILS}
1678
+ ${fragmentShader}`;
1679
+ }
1680
+
1681
+ // src/rendering/postprocess/PostProcessEffect.ts
1682
+ var PostProcessEffect = class {
1683
+ type;
1684
+ name;
1685
+ params;
1686
+ pipeline = null;
1687
+ uniformBuffer = null;
1688
+ bindGroup = null;
1689
+ sampler = null;
1690
+ _initialized = false;
1691
+ constructor(type, name, params) {
1692
+ this.type = type;
1693
+ this.name = name ?? type;
1694
+ this.params = { ...getDefaultParams(type), ...params };
1695
+ }
1696
+ /**
1697
+ * Check if effect is enabled
1698
+ */
1699
+ get enabled() {
1700
+ return this.params.enabled;
1701
+ }
1702
+ /**
1703
+ * Enable/disable effect
1704
+ */
1705
+ set enabled(value) {
1706
+ this.params.enabled = value;
1707
+ }
1708
+ /**
1709
+ * Get effect intensity
1710
+ */
1711
+ get intensity() {
1712
+ return this.params.intensity;
1713
+ }
1714
+ /**
1715
+ * Set effect intensity
1716
+ */
1717
+ set intensity(value) {
1718
+ this.params.intensity = Math.max(0, value);
1719
+ }
1720
+ /**
1721
+ * Get current parameters
1722
+ */
1723
+ getParams() {
1724
+ return this.params;
1725
+ }
1726
+ /**
1727
+ * Update parameters
1728
+ */
1729
+ setParams(params) {
1730
+ this.params = { ...this.params, ...params };
1731
+ }
1732
+ /**
1733
+ * Check if effect is initialized
1734
+ */
1735
+ get initialized() {
1736
+ return this._initialized;
1737
+ }
1738
+ /**
1739
+ * Initialize GPU resources
1740
+ */
1741
+ async initialize(device) {
1742
+ if (this._initialized) return;
1743
+ const uniformSize = UNIFORM_SIZES[this.type];
1744
+ this.uniformBuffer = device.createBuffer({
1745
+ size: uniformSize,
1746
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1747
+ label: `${this.name}_uniforms`
1748
+ });
1749
+ this.sampler = device.createSampler({
1750
+ magFilter: "linear",
1751
+ minFilter: "linear",
1752
+ addressModeU: "clamp-to-edge",
1753
+ addressModeV: "clamp-to-edge",
1754
+ label: `${this.name}_sampler`
1755
+ });
1756
+ await this.createPipeline(device);
1757
+ this._initialized = true;
1758
+ }
1759
+ /**
1760
+ * Dispose GPU resources
1761
+ */
1762
+ dispose() {
1763
+ this.uniformBuffer?.destroy();
1764
+ this.uniformBuffer = null;
1765
+ this.pipeline = null;
1766
+ this.bindGroup = null;
1767
+ this.sampler = null;
1768
+ this._initialized = false;
1769
+ }
1770
+ };
1771
+ var BloomEffect = class extends PostProcessEffect {
1772
+ downsamplePipelines = [];
1773
+ upsamplePipelines = [];
1774
+ mipViews = [];
1775
+ mipTexture = null;
1776
+ constructor(params) {
1777
+ super("bloom", "Bloom", params);
1778
+ }
1779
+ async createPipeline(device) {
1780
+ const bindGroupLayout = device.createBindGroupLayout({
1781
+ label: "bloom_bind_group_layout",
1782
+ entries: [
1783
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
1784
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
1785
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
1786
+ ]
1787
+ });
1788
+ const pipelineLayout = device.createPipelineLayout({
1789
+ label: "bloom_pipeline_layout",
1790
+ bindGroupLayouts: [bindGroupLayout]
1791
+ });
1792
+ const shaderModule = device.createShaderModule({
1793
+ label: "bloom_shader",
1794
+ code: FULLSCREEN_VERTEX_SHADER + BLOOM_SHADER
1795
+ });
1796
+ this.pipeline = device.createRenderPipeline({
1797
+ label: "bloom_pipeline",
1798
+ layout: pipelineLayout,
1799
+ vertex: {
1800
+ module: shaderModule,
1801
+ entryPoint: "vs_main"
1802
+ },
1803
+ fragment: {
1804
+ module: shaderModule,
1805
+ entryPoint: "fs_bloom",
1806
+ targets: [{ format: "rgba16float" }]
1807
+ },
1808
+ primitive: { topology: "triangle-list" }
1809
+ });
1810
+ }
1811
+ updateUniforms(device, frameData) {
1812
+ if (!this.uniformBuffer) return;
1813
+ const p = this.params;
1814
+ const data = new Float32Array([
1815
+ p.intensity,
1816
+ p.threshold,
1817
+ p.softThreshold,
1818
+ p.radius,
1819
+ p.iterations,
1820
+ p.anamorphic,
1821
+ p.highQuality ? 1 : 0,
1822
+ 0,
1823
+ // padding
1824
+ frameData.time,
1825
+ frameData.deltaTime,
1826
+ 0,
1827
+ 0
1828
+ // padding
1829
+ ]);
1830
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
1831
+ }
1832
+ render(context) {
1833
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
1834
+ this.updateUniforms(context.device, context.frameData);
1835
+ const bindGroup = context.device.createBindGroup({
1836
+ layout: this.pipeline.getBindGroupLayout(0),
1837
+ entries: [
1838
+ { binding: 0, resource: this.sampler },
1839
+ { binding: 1, resource: context.input.view },
1840
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
1841
+ ]
1842
+ });
1843
+ const passEncoder = context.commandEncoder.beginRenderPass({
1844
+ colorAttachments: [
1845
+ {
1846
+ view: context.output.view,
1847
+ loadOp: "clear",
1848
+ storeOp: "store",
1849
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
1850
+ }
1851
+ ]
1852
+ });
1853
+ passEncoder.setPipeline(this.pipeline);
1854
+ passEncoder.setBindGroup(0, bindGroup);
1855
+ passEncoder.draw(3);
1856
+ passEncoder.end();
1857
+ }
1858
+ dispose() {
1859
+ super.dispose();
1860
+ this.mipTexture?.destroy();
1861
+ this.mipTexture = null;
1862
+ this.mipViews = [];
1863
+ this.downsamplePipelines = [];
1864
+ this.upsamplePipelines = [];
1865
+ }
1866
+ };
1867
+ var ToneMapEffect = class extends PostProcessEffect {
1868
+ constructor(params) {
1869
+ super("tonemap", "Tone Mapping", params);
1870
+ }
1871
+ async createPipeline(device) {
1872
+ const bindGroupLayout = device.createBindGroupLayout({
1873
+ label: "tonemap_bind_group_layout",
1874
+ entries: [
1875
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
1876
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
1877
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
1878
+ ]
1879
+ });
1880
+ const pipelineLayout = device.createPipelineLayout({
1881
+ label: "tonemap_pipeline_layout",
1882
+ bindGroupLayouts: [bindGroupLayout]
1883
+ });
1884
+ const shaderModule = device.createShaderModule({
1885
+ label: "tonemap_shader",
1886
+ code: FULLSCREEN_VERTEX_SHADER + TONEMAP_SHADER
1887
+ });
1888
+ this.pipeline = device.createRenderPipeline({
1889
+ label: "tonemap_pipeline",
1890
+ layout: pipelineLayout,
1891
+ vertex: {
1892
+ module: shaderModule,
1893
+ entryPoint: "vs_main"
1894
+ },
1895
+ fragment: {
1896
+ module: shaderModule,
1897
+ entryPoint: "fs_tonemap",
1898
+ targets: [{ format: "rgba8unorm" }]
1899
+ },
1900
+ primitive: { topology: "triangle-list" }
1901
+ });
1902
+ }
1903
+ updateUniforms(device, _frameData) {
1904
+ if (!this.uniformBuffer) return;
1905
+ const p = this.params;
1906
+ const operatorMap = {
1907
+ none: 0,
1908
+ reinhard: 1,
1909
+ reinhardLum: 2,
1910
+ aces: 3,
1911
+ acesApprox: 4,
1912
+ filmic: 5,
1913
+ uncharted2: 6,
1914
+ uchimura: 7,
1915
+ lottes: 8,
1916
+ khronos: 9
1917
+ };
1918
+ const data = new Float32Array([
1919
+ operatorMap[p.operator] ?? 3,
1920
+ p.exposure,
1921
+ p.gamma,
1922
+ p.whitePoint,
1923
+ p.contrast,
1924
+ p.saturation,
1925
+ p.intensity,
1926
+ 0
1927
+ // padding
1928
+ ]);
1929
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
1930
+ }
1931
+ render(context) {
1932
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
1933
+ this.updateUniforms(context.device, context.frameData);
1934
+ const bindGroup = context.device.createBindGroup({
1935
+ layout: this.pipeline.getBindGroupLayout(0),
1936
+ entries: [
1937
+ { binding: 0, resource: this.sampler },
1938
+ { binding: 1, resource: context.input.view },
1939
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
1940
+ ]
1941
+ });
1942
+ const passEncoder = context.commandEncoder.beginRenderPass({
1943
+ colorAttachments: [
1944
+ {
1945
+ view: context.output.view,
1946
+ loadOp: "clear",
1947
+ storeOp: "store",
1948
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
1949
+ }
1950
+ ]
1951
+ });
1952
+ passEncoder.setPipeline(this.pipeline);
1953
+ passEncoder.setBindGroup(0, bindGroup);
1954
+ passEncoder.draw(3);
1955
+ passEncoder.end();
1956
+ }
1957
+ };
1958
+ var FXAAEffect = class extends PostProcessEffect {
1959
+ constructor(params) {
1960
+ super("fxaa", "FXAA", params);
1961
+ }
1962
+ async createPipeline(device) {
1963
+ const bindGroupLayout = device.createBindGroupLayout({
1964
+ label: "fxaa_bind_group_layout",
1965
+ entries: [
1966
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
1967
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
1968
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
1969
+ ]
1970
+ });
1971
+ const pipelineLayout = device.createPipelineLayout({
1972
+ label: "fxaa_pipeline_layout",
1973
+ bindGroupLayouts: [bindGroupLayout]
1974
+ });
1975
+ const shaderModule = device.createShaderModule({
1976
+ label: "fxaa_shader",
1977
+ code: FULLSCREEN_VERTEX_SHADER + FXAA_SHADER
1978
+ });
1979
+ this.pipeline = device.createRenderPipeline({
1980
+ label: "fxaa_pipeline",
1981
+ layout: pipelineLayout,
1982
+ vertex: {
1983
+ module: shaderModule,
1984
+ entryPoint: "vs_main"
1985
+ },
1986
+ fragment: {
1987
+ module: shaderModule,
1988
+ entryPoint: "fs_fxaa",
1989
+ targets: [{ format: "rgba8unorm" }]
1990
+ },
1991
+ primitive: { topology: "triangle-list" }
1992
+ });
1993
+ }
1994
+ updateUniforms(device, _frameData) {
1995
+ if (!this.uniformBuffer) return;
1996
+ const p = this.params;
1997
+ const qualityMap = { low: 0, medium: 1, high: 2, ultra: 3 };
1998
+ const data = new Float32Array([
1999
+ qualityMap[p.quality] ?? 2,
2000
+ p.edgeThreshold,
2001
+ p.edgeThresholdMin,
2002
+ p.intensity
2003
+ ]);
2004
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2005
+ }
2006
+ render(context) {
2007
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2008
+ this.updateUniforms(context.device, context.frameData);
2009
+ const bindGroup = context.device.createBindGroup({
2010
+ layout: this.pipeline.getBindGroupLayout(0),
2011
+ entries: [
2012
+ { binding: 0, resource: this.sampler },
2013
+ { binding: 1, resource: context.input.view },
2014
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2015
+ ]
2016
+ });
2017
+ const passEncoder = context.commandEncoder.beginRenderPass({
2018
+ colorAttachments: [
2019
+ {
2020
+ view: context.output.view,
2021
+ loadOp: "clear",
2022
+ storeOp: "store",
2023
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2024
+ }
2025
+ ]
2026
+ });
2027
+ passEncoder.setPipeline(this.pipeline);
2028
+ passEncoder.setBindGroup(0, bindGroup);
2029
+ passEncoder.draw(3);
2030
+ passEncoder.end();
2031
+ }
2032
+ };
2033
+ var VignetteEffect = class extends PostProcessEffect {
2034
+ constructor(params) {
2035
+ super("vignette", "Vignette", params);
2036
+ }
2037
+ async createPipeline(device) {
2038
+ const bindGroupLayout = device.createBindGroupLayout({
2039
+ label: "vignette_bind_group_layout",
2040
+ entries: [
2041
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
2042
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
2043
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
2044
+ ]
2045
+ });
2046
+ const pipelineLayout = device.createPipelineLayout({
2047
+ label: "vignette_pipeline_layout",
2048
+ bindGroupLayouts: [bindGroupLayout]
2049
+ });
2050
+ const shaderModule = device.createShaderModule({
2051
+ label: "vignette_shader",
2052
+ code: FULLSCREEN_VERTEX_SHADER + VIGNETTE_SHADER
2053
+ });
2054
+ this.pipeline = device.createRenderPipeline({
2055
+ label: "vignette_pipeline",
2056
+ layout: pipelineLayout,
2057
+ vertex: {
2058
+ module: shaderModule,
2059
+ entryPoint: "vs_main"
2060
+ },
2061
+ fragment: {
2062
+ module: shaderModule,
2063
+ entryPoint: "fs_vignette",
2064
+ targets: [{ format: "rgba8unorm" }]
2065
+ },
2066
+ primitive: { topology: "triangle-list" }
2067
+ });
2068
+ }
2069
+ updateUniforms(device, _frameData) {
2070
+ if (!this.uniformBuffer) return;
2071
+ const p = this.params;
2072
+ const data = new Float32Array([
2073
+ p.intensity,
2074
+ p.roundness,
2075
+ p.smoothness,
2076
+ 0,
2077
+ // padding
2078
+ p.color[0],
2079
+ p.color[1],
2080
+ p.color[2],
2081
+ 1
2082
+ ]);
2083
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2084
+ }
2085
+ render(context) {
2086
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2087
+ this.updateUniforms(context.device, context.frameData);
2088
+ const bindGroup = context.device.createBindGroup({
2089
+ layout: this.pipeline.getBindGroupLayout(0),
2090
+ entries: [
2091
+ { binding: 0, resource: this.sampler },
2092
+ { binding: 1, resource: context.input.view },
2093
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2094
+ ]
2095
+ });
2096
+ const passEncoder = context.commandEncoder.beginRenderPass({
2097
+ colorAttachments: [
2098
+ {
2099
+ view: context.output.view,
2100
+ loadOp: "clear",
2101
+ storeOp: "store",
2102
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2103
+ }
2104
+ ]
2105
+ });
2106
+ passEncoder.setPipeline(this.pipeline);
2107
+ passEncoder.setBindGroup(0, bindGroup);
2108
+ passEncoder.draw(3);
2109
+ passEncoder.end();
2110
+ }
2111
+ };
2112
+ var FilmGrainEffect = class extends PostProcessEffect {
2113
+ constructor(params) {
2114
+ super("filmGrain", "Film Grain", params);
2115
+ }
2116
+ async createPipeline(device) {
2117
+ const bindGroupLayout = device.createBindGroupLayout({
2118
+ label: "filmgrain_bind_group_layout",
2119
+ entries: [
2120
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
2121
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
2122
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
2123
+ ]
2124
+ });
2125
+ const pipelineLayout = device.createPipelineLayout({
2126
+ label: "filmgrain_pipeline_layout",
2127
+ bindGroupLayouts: [bindGroupLayout]
2128
+ });
2129
+ const shaderModule = device.createShaderModule({
2130
+ label: "filmgrain_shader",
2131
+ code: FULLSCREEN_VERTEX_SHADER + FILM_GRAIN_SHADER
2132
+ });
2133
+ this.pipeline = device.createRenderPipeline({
2134
+ label: "filmgrain_pipeline",
2135
+ layout: pipelineLayout,
2136
+ vertex: {
2137
+ module: shaderModule,
2138
+ entryPoint: "vs_main"
2139
+ },
2140
+ fragment: {
2141
+ module: shaderModule,
2142
+ entryPoint: "fs_filmgrain",
2143
+ targets: [{ format: "rgba8unorm" }]
2144
+ },
2145
+ primitive: { topology: "triangle-list" }
2146
+ });
2147
+ }
2148
+ updateUniforms(device, frameData) {
2149
+ if (!this.uniformBuffer) return;
2150
+ const p = this.params;
2151
+ const data = new Float32Array([
2152
+ p.intensity,
2153
+ p.size,
2154
+ p.luminanceContribution,
2155
+ p.animated ? frameData.time : 0
2156
+ ]);
2157
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2158
+ }
2159
+ render(context) {
2160
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2161
+ this.updateUniforms(context.device, context.frameData);
2162
+ const bindGroup = context.device.createBindGroup({
2163
+ layout: this.pipeline.getBindGroupLayout(0),
2164
+ entries: [
2165
+ { binding: 0, resource: this.sampler },
2166
+ { binding: 1, resource: context.input.view },
2167
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2168
+ ]
2169
+ });
2170
+ const passEncoder = context.commandEncoder.beginRenderPass({
2171
+ colorAttachments: [
2172
+ {
2173
+ view: context.output.view,
2174
+ loadOp: "clear",
2175
+ storeOp: "store",
2176
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2177
+ }
2178
+ ]
2179
+ });
2180
+ passEncoder.setPipeline(this.pipeline);
2181
+ passEncoder.setBindGroup(0, bindGroup);
2182
+ passEncoder.draw(3);
2183
+ passEncoder.end();
2184
+ }
2185
+ };
2186
+ var SharpenEffect = class extends PostProcessEffect {
2187
+ constructor(params) {
2188
+ super("sharpen", "Sharpen", params);
2189
+ }
2190
+ async createPipeline(device) {
2191
+ const bindGroupLayout = device.createBindGroupLayout({
2192
+ label: "sharpen_bind_group_layout",
2193
+ entries: [
2194
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
2195
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
2196
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
2197
+ ]
2198
+ });
2199
+ const pipelineLayout = device.createPipelineLayout({
2200
+ label: "sharpen_pipeline_layout",
2201
+ bindGroupLayouts: [bindGroupLayout]
2202
+ });
2203
+ const shaderModule = device.createShaderModule({
2204
+ label: "sharpen_shader",
2205
+ code: FULLSCREEN_VERTEX_SHADER + SHARPEN_SHADER
2206
+ });
2207
+ this.pipeline = device.createRenderPipeline({
2208
+ label: "sharpen_pipeline",
2209
+ layout: pipelineLayout,
2210
+ vertex: {
2211
+ module: shaderModule,
2212
+ entryPoint: "vs_main"
2213
+ },
2214
+ fragment: {
2215
+ module: shaderModule,
2216
+ entryPoint: "fs_sharpen",
2217
+ targets: [{ format: "rgba8unorm" }]
2218
+ },
2219
+ primitive: { topology: "triangle-list" }
2220
+ });
2221
+ }
2222
+ updateUniforms(device, _frameData) {
2223
+ if (!this.uniformBuffer) return;
2224
+ const p = this.params;
2225
+ const data = new Float32Array([
2226
+ p.intensity,
2227
+ p.amount,
2228
+ p.threshold,
2229
+ 0
2230
+ // padding
2231
+ ]);
2232
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2233
+ }
2234
+ render(context) {
2235
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2236
+ this.updateUniforms(context.device, context.frameData);
2237
+ const bindGroup = context.device.createBindGroup({
2238
+ layout: this.pipeline.getBindGroupLayout(0),
2239
+ entries: [
2240
+ { binding: 0, resource: this.sampler },
2241
+ { binding: 1, resource: context.input.view },
2242
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2243
+ ]
2244
+ });
2245
+ const passEncoder = context.commandEncoder.beginRenderPass({
2246
+ colorAttachments: [
2247
+ {
2248
+ view: context.output.view,
2249
+ loadOp: "clear",
2250
+ storeOp: "store",
2251
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2252
+ }
2253
+ ]
2254
+ });
2255
+ passEncoder.setPipeline(this.pipeline);
2256
+ passEncoder.setBindGroup(0, bindGroup);
2257
+ passEncoder.draw(3);
2258
+ passEncoder.end();
2259
+ }
2260
+ };
2261
+ var ChromaticAberrationEffect = class extends PostProcessEffect {
2262
+ constructor(params) {
2263
+ super("chromaticAberration", "Chromatic Aberration", params);
2264
+ }
2265
+ async createPipeline(device) {
2266
+ const bindGroupLayout = device.createBindGroupLayout({
2267
+ label: "chromatic_bind_group_layout",
2268
+ entries: [
2269
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
2270
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
2271
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
2272
+ ]
2273
+ });
2274
+ const pipelineLayout = device.createPipelineLayout({
2275
+ label: "chromatic_pipeline_layout",
2276
+ bindGroupLayouts: [bindGroupLayout]
2277
+ });
2278
+ const shaderModule = device.createShaderModule({
2279
+ label: "chromatic_shader",
2280
+ code: FULLSCREEN_VERTEX_SHADER + CHROMATIC_ABERRATION_SHADER
2281
+ });
2282
+ this.pipeline = device.createRenderPipeline({
2283
+ label: "chromatic_pipeline",
2284
+ layout: pipelineLayout,
2285
+ vertex: {
2286
+ module: shaderModule,
2287
+ entryPoint: "vs_main"
2288
+ },
2289
+ fragment: {
2290
+ module: shaderModule,
2291
+ entryPoint: "fs_chromatic",
2292
+ targets: [{ format: "rgba8unorm" }]
2293
+ },
2294
+ primitive: { topology: "triangle-list" }
2295
+ });
2296
+ }
2297
+ updateUniforms(device, _frameData) {
2298
+ if (!this.uniformBuffer) return;
2299
+ const p = this.params;
2300
+ const data = new Float32Array([
2301
+ p.intensity,
2302
+ p.radial ? 1 : 0,
2303
+ 0,
2304
+ 0,
2305
+ // padding
2306
+ p.redOffset[0],
2307
+ p.redOffset[1],
2308
+ p.greenOffset[0],
2309
+ p.greenOffset[1],
2310
+ p.blueOffset[0],
2311
+ p.blueOffset[1],
2312
+ 0,
2313
+ 0
2314
+ // padding
2315
+ ]);
2316
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2317
+ }
2318
+ render(context) {
2319
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2320
+ this.updateUniforms(context.device, context.frameData);
2321
+ const bindGroup = context.device.createBindGroup({
2322
+ layout: this.pipeline.getBindGroupLayout(0),
2323
+ entries: [
2324
+ { binding: 0, resource: this.sampler },
2325
+ { binding: 1, resource: context.input.view },
2326
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2327
+ ]
2328
+ });
2329
+ const passEncoder = context.commandEncoder.beginRenderPass({
2330
+ colorAttachments: [
2331
+ {
2332
+ view: context.output.view,
2333
+ loadOp: "clear",
2334
+ storeOp: "store",
2335
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2336
+ }
2337
+ ]
2338
+ });
2339
+ passEncoder.setPipeline(this.pipeline);
2340
+ passEncoder.setBindGroup(0, bindGroup);
2341
+ passEncoder.draw(3);
2342
+ passEncoder.end();
2343
+ }
2344
+ };
2345
+ var CausticsEffect = class extends PostProcessEffect {
2346
+ constructor(params) {
2347
+ super("caustics", "Caustics", params);
2348
+ }
2349
+ async createPipeline(device) {
2350
+ const bindGroupLayout = device.createBindGroupLayout({
2351
+ label: "caustics_bind_group_layout",
2352
+ entries: [
2353
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
2354
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
2355
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
2356
+ ]
2357
+ });
2358
+ const pipelineLayout = device.createPipelineLayout({
2359
+ label: "caustics_pipeline_layout",
2360
+ bindGroupLayouts: [bindGroupLayout]
2361
+ });
2362
+ const shaderModule = device.createShaderModule({
2363
+ label: "caustics_shader",
2364
+ code: FULLSCREEN_VERTEX_SHADER + CAUSTICS_SHADER
2365
+ });
2366
+ this.pipeline = device.createRenderPipeline({
2367
+ label: "caustics_pipeline",
2368
+ layout: pipelineLayout,
2369
+ vertex: { module: shaderModule, entryPoint: "vs_main" },
2370
+ fragment: {
2371
+ module: shaderModule,
2372
+ entryPoint: "fs_caustics",
2373
+ targets: [{ format: "rgba16float" }]
2374
+ },
2375
+ primitive: { topology: "triangle-list" }
2376
+ });
2377
+ }
2378
+ updateUniforms(device, frameData) {
2379
+ if (!this.uniformBuffer) return;
2380
+ const p = this.params;
2381
+ const data = new Float32Array([
2382
+ p.intensity,
2383
+ p.scale,
2384
+ p.speed,
2385
+ p.depthFade,
2386
+ p.color[0],
2387
+ p.color[1],
2388
+ p.color[2],
2389
+ p.waterLevel,
2390
+ frameData.time,
2391
+ p.dispersion ?? 0,
2392
+ p.foamIntensity ?? 0,
2393
+ p.shadowStrength ?? 0,
2394
+ 0,
2395
+ 0,
2396
+ 0,
2397
+ 0
2398
+ ]);
2399
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2400
+ }
2401
+ render(context) {
2402
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2403
+ this.updateUniforms(context.device, context.frameData);
2404
+ const bindGroup = context.device.createBindGroup({
2405
+ layout: this.pipeline.getBindGroupLayout(0),
2406
+ entries: [
2407
+ { binding: 0, resource: this.sampler },
2408
+ { binding: 1, resource: context.input.view },
2409
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2410
+ ]
2411
+ });
2412
+ const passEncoder = context.commandEncoder.beginRenderPass({
2413
+ colorAttachments: [
2414
+ {
2415
+ view: context.output.view,
2416
+ loadOp: "clear",
2417
+ storeOp: "store",
2418
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2419
+ }
2420
+ ]
2421
+ });
2422
+ passEncoder.setPipeline(this.pipeline);
2423
+ passEncoder.setBindGroup(0, bindGroup);
2424
+ passEncoder.draw(3);
2425
+ passEncoder.end();
2426
+ }
2427
+ };
2428
+ var SSREffect = class extends PostProcessEffect {
2429
+ constructor(params) {
2430
+ super("ssr", "SSR", params);
2431
+ }
2432
+ async createPipeline(device) {
2433
+ const bindGroupLayout = device.createBindGroupLayout({
2434
+ label: "ssr_bind_group_layout",
2435
+ entries: [
2436
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
2437
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
2438
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
2439
+ ]
2440
+ });
2441
+ const pipelineLayout = device.createPipelineLayout({
2442
+ label: "ssr_pipeline_layout",
2443
+ bindGroupLayouts: [bindGroupLayout]
2444
+ });
2445
+ const shaderModule = device.createShaderModule({
2446
+ label: "ssr_shader",
2447
+ code: FULLSCREEN_VERTEX_SHADER + SSR_SHADER
2448
+ });
2449
+ this.pipeline = device.createRenderPipeline({
2450
+ label: "ssr_pipeline",
2451
+ layout: pipelineLayout,
2452
+ vertex: { module: shaderModule, entryPoint: "vs_main" },
2453
+ fragment: {
2454
+ module: shaderModule,
2455
+ entryPoint: "fs_ssr",
2456
+ targets: [{ format: "rgba16float" }]
2457
+ },
2458
+ primitive: { topology: "triangle-list" }
2459
+ });
2460
+ }
2461
+ updateUniforms(device, _frameData) {
2462
+ if (!this.uniformBuffer) return;
2463
+ const p = this.params;
2464
+ const data = new Float32Array([
2465
+ p.intensity,
2466
+ p.maxSteps,
2467
+ p.stepSize,
2468
+ p.thickness,
2469
+ p.roughnessFade,
2470
+ p.edgeFade,
2471
+ p.roughnessBlur ?? 0,
2472
+ p.fresnelStrength ?? 0,
2473
+ 0,
2474
+ 0,
2475
+ 0,
2476
+ 0
2477
+ ]);
2478
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2479
+ }
2480
+ render(context) {
2481
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2482
+ this.updateUniforms(context.device, context.frameData);
2483
+ const bindGroup = context.device.createBindGroup({
2484
+ layout: this.pipeline.getBindGroupLayout(0),
2485
+ entries: [
2486
+ { binding: 0, resource: this.sampler },
2487
+ { binding: 1, resource: context.input.view },
2488
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2489
+ ]
2490
+ });
2491
+ const passEncoder = context.commandEncoder.beginRenderPass({
2492
+ colorAttachments: [
2493
+ {
2494
+ view: context.output.view,
2495
+ loadOp: "clear",
2496
+ storeOp: "store",
2497
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2498
+ }
2499
+ ]
2500
+ });
2501
+ passEncoder.setPipeline(this.pipeline);
2502
+ passEncoder.setBindGroup(0, bindGroup);
2503
+ passEncoder.draw(3);
2504
+ passEncoder.end();
2505
+ }
2506
+ };
2507
+ var SSAOEffect = class extends PostProcessEffect {
2508
+ constructor(params) {
2509
+ super("ssao", "SSAO", params);
2510
+ }
2511
+ async createPipeline(device) {
2512
+ const bindGroupLayout = device.createBindGroupLayout({
2513
+ label: "ssao_bind_group_layout",
2514
+ entries: [
2515
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
2516
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
2517
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
2518
+ ]
2519
+ });
2520
+ const pipelineLayout = device.createPipelineLayout({
2521
+ label: "ssao_pipeline_layout",
2522
+ bindGroupLayouts: [bindGroupLayout]
2523
+ });
2524
+ const shaderModule = device.createShaderModule({
2525
+ label: "ssao_shader",
2526
+ code: FULLSCREEN_VERTEX_SHADER + SSAO_SHADER
2527
+ });
2528
+ this.pipeline = device.createRenderPipeline({
2529
+ label: "ssao_pipeline",
2530
+ layout: pipelineLayout,
2531
+ vertex: { module: shaderModule, entryPoint: "vs_main" },
2532
+ fragment: {
2533
+ module: shaderModule,
2534
+ entryPoint: "fs_ssao",
2535
+ targets: [{ format: "rgba16float" }]
2536
+ },
2537
+ primitive: { topology: "triangle-list" }
2538
+ });
2539
+ }
2540
+ updateUniforms(device, _frameData) {
2541
+ if (!this.uniformBuffer) return;
2542
+ const p = this.params;
2543
+ const modeVal = (p.mode ?? "hemisphere") === "hbao" ? 1 : 0;
2544
+ const data = new Float32Array([
2545
+ p.intensity,
2546
+ p.radius,
2547
+ p.bias,
2548
+ p.samples,
2549
+ p.power,
2550
+ p.falloff,
2551
+ 0,
2552
+ 0,
2553
+ modeVal,
2554
+ p.bentNormals ? 1 : 0,
2555
+ p.spatialDenoise ? 1 : 0,
2556
+ 0
2557
+ ]);
2558
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2559
+ }
2560
+ render(context) {
2561
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2562
+ this.updateUniforms(context.device, context.frameData);
2563
+ const bindGroup = context.device.createBindGroup({
2564
+ layout: this.pipeline.getBindGroupLayout(0),
2565
+ entries: [
2566
+ { binding: 0, resource: this.sampler },
2567
+ { binding: 1, resource: context.input.view },
2568
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2569
+ ]
2570
+ });
2571
+ const passEncoder = context.commandEncoder.beginRenderPass({
2572
+ colorAttachments: [
2573
+ {
2574
+ view: context.output.view,
2575
+ loadOp: "clear",
2576
+ storeOp: "store",
2577
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2578
+ }
2579
+ ]
2580
+ });
2581
+ passEncoder.setPipeline(this.pipeline);
2582
+ passEncoder.setBindGroup(0, bindGroup);
2583
+ passEncoder.draw(3);
2584
+ passEncoder.end();
2585
+ }
2586
+ };
2587
+ var SSGIEffect = class extends PostProcessEffect {
2588
+ constructor(params) {
2589
+ super("ssgi", "SSGI", params);
2590
+ }
2591
+ async createPipeline(device) {
2592
+ const bindGroupLayout = device.createBindGroupLayout({
2593
+ label: "ssgi_bind_group_layout",
2594
+ entries: [
2595
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
2596
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
2597
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
2598
+ ]
2599
+ });
2600
+ const pipelineLayout = device.createPipelineLayout({
2601
+ label: "ssgi_pipeline_layout",
2602
+ bindGroupLayouts: [bindGroupLayout]
2603
+ });
2604
+ const shaderModule = device.createShaderModule({
2605
+ label: "ssgi_shader",
2606
+ code: FULLSCREEN_VERTEX_SHADER + SSGI_SHADER
2607
+ });
2608
+ this.pipeline = device.createRenderPipeline({
2609
+ label: "ssgi_pipeline",
2610
+ layout: pipelineLayout,
2611
+ vertex: { module: shaderModule, entryPoint: "vs_main" },
2612
+ fragment: {
2613
+ module: shaderModule,
2614
+ entryPoint: "fs_ssgi",
2615
+ targets: [{ format: "rgba16float" }]
2616
+ },
2617
+ primitive: { topology: "triangle-list" }
2618
+ });
2619
+ }
2620
+ updateUniforms(device, _frameData) {
2621
+ if (!this.uniformBuffer) return;
2622
+ const p = this.params;
2623
+ const data = new Float32Array([
2624
+ p.intensity,
2625
+ p.radius,
2626
+ p.samples,
2627
+ p.bounceIntensity,
2628
+ p.falloff,
2629
+ p.temporalBlend ?? 0,
2630
+ p.spatialDenoise ? 1 : 0,
2631
+ p.multiBounce ?? 0,
2632
+ 0,
2633
+ 0,
2634
+ 0,
2635
+ 0
2636
+ ]);
2637
+ device.queue.writeBuffer(this.uniformBuffer, 0, data);
2638
+ }
2639
+ render(context) {
2640
+ if (!this.enabled || !this.pipeline || !this.uniformBuffer || !this.sampler) return;
2641
+ this.updateUniforms(context.device, context.frameData);
2642
+ const bindGroup = context.device.createBindGroup({
2643
+ layout: this.pipeline.getBindGroupLayout(0),
2644
+ entries: [
2645
+ { binding: 0, resource: this.sampler },
2646
+ { binding: 1, resource: context.input.view },
2647
+ { binding: 2, resource: { buffer: this.uniformBuffer } }
2648
+ ]
2649
+ });
2650
+ const passEncoder = context.commandEncoder.beginRenderPass({
2651
+ colorAttachments: [
2652
+ {
2653
+ view: context.output.view,
2654
+ loadOp: "clear",
2655
+ storeOp: "store",
2656
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2657
+ }
2658
+ ]
2659
+ });
2660
+ passEncoder.setPipeline(this.pipeline);
2661
+ passEncoder.setBindGroup(0, bindGroup);
2662
+ passEncoder.draw(3);
2663
+ passEncoder.end();
2664
+ }
2665
+ };
2666
+ function createEffect(type, params) {
2667
+ switch (type) {
2668
+ case "bloom":
2669
+ return new BloomEffect(params);
2670
+ case "tonemap":
2671
+ return new ToneMapEffect(params);
2672
+ case "fxaa":
2673
+ return new FXAAEffect(params);
2674
+ case "vignette":
2675
+ return new VignetteEffect(params);
2676
+ case "filmGrain":
2677
+ return new FilmGrainEffect(params);
2678
+ case "sharpen":
2679
+ return new SharpenEffect(params);
2680
+ case "chromaticAberration":
2681
+ return new ChromaticAberrationEffect(params);
2682
+ case "caustics":
2683
+ return new CausticsEffect(params);
2684
+ case "ssr":
2685
+ return new SSREffect(params);
2686
+ case "ssao":
2687
+ return new SSAOEffect(params);
2688
+ case "ssgi":
2689
+ return new SSGIEffect(params);
2690
+ default:
2691
+ throw new Error(`Unknown effect type: ${type}`);
2692
+ }
2693
+ }
2694
+
2695
+ // src/rendering/postprocess/PostProcessPipeline.ts
2696
+ var DEFAULT_PIPELINE_CONFIG = {
2697
+ hdrEnabled: true,
2698
+ hdrFormat: "rgba16float",
2699
+ ldrFormat: "rgba8unorm",
2700
+ msaaSamples: 1,
2701
+ effects: [],
2702
+ autoResize: true
2703
+ };
2704
+ var PostProcessPipeline = class {
2705
+ device = null;
2706
+ config;
2707
+ effects = [];
2708
+ renderTargets = /* @__PURE__ */ new Map();
2709
+ pingPongTargets = [null, null];
2710
+ currentWidth = 0;
2711
+ currentHeight = 0;
2712
+ _initialized = false;
2713
+ frameCount = 0;
2714
+ constructor(config) {
2715
+ this.config = { ...DEFAULT_PIPELINE_CONFIG, ...config };
2716
+ }
2717
+ /**
2718
+ * Check if pipeline is initialized
2719
+ */
2720
+ get initialized() {
2721
+ return this._initialized;
2722
+ }
2723
+ /**
2724
+ * Get current configuration
2725
+ */
2726
+ getConfig() {
2727
+ return { ...this.config };
2728
+ }
2729
+ /**
2730
+ * Get list of active effects
2731
+ */
2732
+ getEffects() {
2733
+ return [...this.effects];
2734
+ }
2735
+ /**
2736
+ * Get effect by name
2737
+ */
2738
+ getEffect(name) {
2739
+ return this.effects.find((e) => e.name === name);
2740
+ }
2741
+ /**
2742
+ * Get effect by type
2743
+ */
2744
+ getEffectByType(type) {
2745
+ return this.effects.find((e) => e.type === type);
2746
+ }
2747
+ /**
2748
+ * Initialize the pipeline with GPU device
2749
+ */
2750
+ async initialize(device, width, height) {
2751
+ if (this._initialized && this.device === device) {
2752
+ if (width !== this.currentWidth || height !== this.currentHeight) {
2753
+ await this.resize(width, height);
2754
+ }
2755
+ return;
2756
+ }
2757
+ this.device = device;
2758
+ this.currentWidth = width;
2759
+ this.currentHeight = height;
2760
+ await this.createRenderTargets();
2761
+ for (const effectConfig of this.config.effects) {
2762
+ await this.addEffect(effectConfig);
2763
+ }
2764
+ this._initialized = true;
2765
+ }
2766
+ /**
2767
+ * Create or recreate render targets
2768
+ */
2769
+ async createRenderTargets() {
2770
+ if (!this.device) return;
2771
+ this.disposeRenderTargets();
2772
+ const format = this.config.hdrEnabled ? this.config.hdrFormat : this.config.ldrFormat;
2773
+ for (let i = 0; i < 2; i++) {
2774
+ const texture = this.device.createTexture({
2775
+ size: { width: this.currentWidth, height: this.currentHeight },
2776
+ format,
2777
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
2778
+ label: `postprocess_pingpong_${i}`
2779
+ });
2780
+ this.pingPongTargets[i] = {
2781
+ id: `pingpong_${i}`,
2782
+ texture,
2783
+ view: texture.createView(),
2784
+ config: {
2785
+ width: this.currentWidth,
2786
+ height: this.currentHeight,
2787
+ format
2788
+ }
2789
+ };
2790
+ }
2791
+ }
2792
+ /**
2793
+ * Dispose render targets
2794
+ */
2795
+ disposeRenderTargets() {
2796
+ for (const target of this.renderTargets.values()) {
2797
+ target.texture.destroy();
2798
+ }
2799
+ this.renderTargets.clear();
2800
+ for (const target of this.pingPongTargets) {
2801
+ target?.texture.destroy();
2802
+ }
2803
+ this.pingPongTargets = [null, null];
2804
+ }
2805
+ /**
2806
+ * Resize render targets
2807
+ */
2808
+ async resize(width, height) {
2809
+ if (width === this.currentWidth && height === this.currentHeight) return;
2810
+ this.currentWidth = width;
2811
+ this.currentHeight = height;
2812
+ await this.createRenderTargets();
2813
+ }
2814
+ /**
2815
+ * Add an effect to the pipeline
2816
+ */
2817
+ async addEffect(config) {
2818
+ const effect = createEffect(config.type, config.params);
2819
+ if (config.name) {
2820
+ effect.name = config.name;
2821
+ }
2822
+ if (this.device) {
2823
+ await effect.initialize(this.device);
2824
+ }
2825
+ if (config.order !== void 0 && config.order < this.effects.length) {
2826
+ this.effects.splice(config.order, 0, effect);
2827
+ } else {
2828
+ this.effects.push(effect);
2829
+ }
2830
+ return effect;
2831
+ }
2832
+ /**
2833
+ * Remove an effect from the pipeline
2834
+ */
2835
+ removeEffect(nameOrType) {
2836
+ const index = this.effects.findIndex((e) => e.name === nameOrType || e.type === nameOrType);
2837
+ if (index === -1) return false;
2838
+ const effect = this.effects[index];
2839
+ effect.dispose();
2840
+ this.effects.splice(index, 1);
2841
+ return true;
2842
+ }
2843
+ /**
2844
+ * Reorder effects
2845
+ */
2846
+ reorderEffects(order) {
2847
+ const newEffects = [];
2848
+ for (const name of order) {
2849
+ const effect = this.effects.find((e) => e.name === name);
2850
+ if (effect) {
2851
+ newEffects.push(effect);
2852
+ }
2853
+ }
2854
+ for (const effect of this.effects) {
2855
+ if (!order.includes(effect.name)) {
2856
+ newEffects.push(effect);
2857
+ }
2858
+ }
2859
+ this.effects = newEffects;
2860
+ }
2861
+ /**
2862
+ * Update effect parameters
2863
+ */
2864
+ updateEffectParams(nameOrType, params) {
2865
+ const effect = this.effects.find((e) => e.name === nameOrType || e.type === nameOrType);
2866
+ if (!effect) return false;
2867
+ effect.setParams(params);
2868
+ return true;
2869
+ }
2870
+ /**
2871
+ * Enable/disable an effect
2872
+ */
2873
+ setEffectEnabled(nameOrType, enabled) {
2874
+ const effect = this.effects.find((e) => e.name === nameOrType || e.type === nameOrType);
2875
+ if (!effect) return false;
2876
+ effect.enabled = enabled;
2877
+ return true;
2878
+ }
2879
+ /**
2880
+ * Execute the post-processing pipeline
2881
+ *
2882
+ * @param commandEncoder GPU command encoder
2883
+ * @param inputView Input texture view (scene render)
2884
+ * @param outputView Output texture view (swap chain)
2885
+ * @param frameData Frame timing and camera data
2886
+ */
2887
+ render(commandEncoder, inputView, outputView, frameData = {}) {
2888
+ if (!this._initialized || !this.device) {
2889
+ console.warn("PostProcessPipeline not initialized");
2890
+ return;
2891
+ }
2892
+ const enabledEffects = this.effects.filter((e) => e.enabled);
2893
+ if (enabledEffects.length === 0) {
2894
+ this.copyTexture(commandEncoder, inputView, outputView);
2895
+ return;
2896
+ }
2897
+ const fullFrameData = {
2898
+ time: frameData.time ?? performance.now() / 1e3,
2899
+ deltaTime: frameData.deltaTime ?? 1 / 60,
2900
+ frameCount: this.frameCount++,
2901
+ resolution: [this.currentWidth, this.currentHeight],
2902
+ nearPlane: frameData.nearPlane ?? 0.1,
2903
+ farPlane: frameData.farPlane ?? 1e3,
2904
+ cameraPosition: frameData.cameraPosition,
2905
+ viewMatrix: frameData.viewMatrix,
2906
+ projectionMatrix: frameData.projectionMatrix,
2907
+ prevViewMatrix: frameData.prevViewMatrix,
2908
+ jitter: frameData.jitter
2909
+ };
2910
+ let currentInput = this.pingPongTargets[0];
2911
+ let currentOutput = this.pingPongTargets[1];
2912
+ let pingPongIndex = 0;
2913
+ this.copyToTarget(commandEncoder, inputView, currentInput);
2914
+ for (let i = 0; i < enabledEffects.length; i++) {
2915
+ const effect = enabledEffects[i];
2916
+ const isLast = i === enabledEffects.length - 1;
2917
+ const output = isLast ? this.createTempTarget(outputView) : currentOutput;
2918
+ const context = {
2919
+ device: this.device,
2920
+ commandEncoder,
2921
+ frameData: fullFrameData,
2922
+ input: currentInput,
2923
+ output
2924
+ };
2925
+ effect.render(context);
2926
+ if (!isLast) {
2927
+ pingPongIndex = 1 - pingPongIndex;
2928
+ currentInput = currentOutput;
2929
+ currentOutput = this.pingPongTargets[pingPongIndex];
2930
+ }
2931
+ }
2932
+ }
2933
+ /**
2934
+ * Create temporary render target wrapper for output view
2935
+ */
2936
+ createTempTarget(view) {
2937
+ return {
2938
+ id: "output",
2939
+ texture: null,
2940
+ // Not needed for output
2941
+ view,
2942
+ config: {
2943
+ width: this.currentWidth,
2944
+ height: this.currentHeight,
2945
+ format: this.config.ldrFormat
2946
+ }
2947
+ };
2948
+ }
2949
+ /**
2950
+ * Copy texture using a blit pass
2951
+ */
2952
+ copyTexture(commandEncoder, source, dest) {
2953
+ const passEncoder = commandEncoder.beginRenderPass({
2954
+ colorAttachments: [
2955
+ {
2956
+ view: dest,
2957
+ loadOp: "clear",
2958
+ storeOp: "store",
2959
+ clearValue: { r: 0, g: 0, b: 0, a: 1 }
2960
+ }
2961
+ ]
2962
+ });
2963
+ passEncoder.end();
2964
+ }
2965
+ /**
2966
+ * Copy input view to a render target
2967
+ */
2968
+ copyToTarget(commandEncoder, source, target) {
2969
+ this.copyTexture(commandEncoder, source, target.view);
2970
+ }
2971
+ /**
2972
+ * Create a preset pipeline configuration
2973
+ */
2974
+ static createPreset(preset) {
2975
+ switch (preset) {
2976
+ case "minimal":
2977
+ return {
2978
+ hdrEnabled: false,
2979
+ effects: [
2980
+ {
2981
+ type: "fxaa",
2982
+ params: {
2983
+ enabled: true,
2984
+ intensity: 1,
2985
+ quality: "medium",
2986
+ edgeThreshold: 0.166,
2987
+ edgeThresholdMin: 0.0833
2988
+ }
2989
+ }
2990
+ ]
2991
+ };
2992
+ case "standard":
2993
+ return {
2994
+ hdrEnabled: true,
2995
+ effects: [
2996
+ {
2997
+ type: "bloom",
2998
+ params: {
2999
+ enabled: true,
3000
+ intensity: 0.5,
3001
+ threshold: 1,
3002
+ softThreshold: 0.5,
3003
+ radius: 4,
3004
+ iterations: 5,
3005
+ anamorphic: 0,
3006
+ highQuality: false
3007
+ }
3008
+ },
3009
+ {
3010
+ type: "tonemap",
3011
+ params: {
3012
+ enabled: true,
3013
+ intensity: 1,
3014
+ operator: "aces",
3015
+ exposure: 1,
3016
+ gamma: 2.2,
3017
+ whitePoint: 1,
3018
+ contrast: 1,
3019
+ saturation: 1
3020
+ }
3021
+ },
3022
+ {
3023
+ type: "fxaa",
3024
+ params: {
3025
+ enabled: true,
3026
+ intensity: 1,
3027
+ quality: "high",
3028
+ edgeThreshold: 0.166,
3029
+ edgeThresholdMin: 0.0833
3030
+ }
3031
+ }
3032
+ ]
3033
+ };
3034
+ case "cinematic":
3035
+ return {
3036
+ hdrEnabled: true,
3037
+ effects: [
3038
+ {
3039
+ type: "bloom",
3040
+ params: {
3041
+ enabled: true,
3042
+ intensity: 0.8,
3043
+ threshold: 0.8,
3044
+ softThreshold: 0.5,
3045
+ radius: 6,
3046
+ iterations: 6,
3047
+ anamorphic: 0.2,
3048
+ highQuality: true
3049
+ }
3050
+ },
3051
+ {
3052
+ type: "tonemap",
3053
+ params: {
3054
+ enabled: true,
3055
+ intensity: 1,
3056
+ operator: "aces",
3057
+ exposure: 1.1,
3058
+ gamma: 2.2,
3059
+ whitePoint: 1,
3060
+ contrast: 1.05,
3061
+ saturation: 1.1
3062
+ }
3063
+ },
3064
+ {
3065
+ type: "vignette",
3066
+ params: {
3067
+ enabled: true,
3068
+ intensity: 0.3,
3069
+ roundness: 1.2,
3070
+ smoothness: 0.4,
3071
+ color: [0, 0, 0]
3072
+ }
3073
+ },
3074
+ {
3075
+ type: "filmGrain",
3076
+ params: {
3077
+ enabled: true,
3078
+ intensity: 0.05,
3079
+ size: 1.5,
3080
+ luminanceContribution: 0.8,
3081
+ animated: true
3082
+ }
3083
+ },
3084
+ {
3085
+ type: "fxaa",
3086
+ params: {
3087
+ enabled: true,
3088
+ intensity: 1,
3089
+ quality: "ultra",
3090
+ edgeThreshold: 0.125,
3091
+ edgeThresholdMin: 0.0625
3092
+ }
3093
+ }
3094
+ ]
3095
+ };
3096
+ case "performance":
3097
+ return {
3098
+ hdrEnabled: false,
3099
+ msaaSamples: 1,
3100
+ effects: [
3101
+ {
3102
+ type: "tonemap",
3103
+ params: {
3104
+ enabled: true,
3105
+ intensity: 1,
3106
+ operator: "reinhardLum",
3107
+ exposure: 1,
3108
+ gamma: 2.2,
3109
+ whitePoint: 1,
3110
+ contrast: 1,
3111
+ saturation: 1
3112
+ }
3113
+ }
3114
+ ]
3115
+ };
3116
+ }
3117
+ }
3118
+ /**
3119
+ * Dispose all resources
3120
+ */
3121
+ dispose() {
3122
+ for (const effect of this.effects) {
3123
+ effect.dispose();
3124
+ }
3125
+ this.effects = [];
3126
+ this.disposeRenderTargets();
3127
+ this.device = null;
3128
+ this._initialized = false;
3129
+ }
3130
+ /**
3131
+ * Get performance statistics
3132
+ */
3133
+ getStats() {
3134
+ const bytesPerPixel = this.config.hdrEnabled ? 8 : 4;
3135
+ const pixelCount = this.currentWidth * this.currentHeight;
3136
+ const renderTargetMemory = (this.pingPongTargets.filter((t) => t !== null).length + this.renderTargets.size) * pixelCount * bytesPerPixel;
3137
+ return {
3138
+ effectCount: this.effects.length,
3139
+ enabledEffects: this.effects.filter((e) => e.enabled).length,
3140
+ renderTargetCount: this.renderTargets.size + 2,
3141
+ estimatedMemoryMB: renderTargetMemory / (1024 * 1024)
3142
+ };
3143
+ }
3144
+ };
3145
+ function createPostProcessPipeline(preset, customConfig) {
3146
+ const presetConfig = preset ? PostProcessPipeline.createPreset(preset) : {};
3147
+ return new PostProcessPipeline({ ...presetConfig, ...customConfig });
3148
+ }
3149
+ function createHDRPipeline() {
3150
+ return createPostProcessPipeline("standard");
3151
+ }
3152
+ function createLDRPipeline() {
3153
+ return createPostProcessPipeline("minimal");
3154
+ }
3155
+
3156
+ // src/rendering/PostProcessing.ts
3157
+ function createDefaultProfile(id, name) {
3158
+ return {
3159
+ id,
3160
+ name,
3161
+ bloom: { enabled: false, threshold: 0.8, intensity: 0.5, radius: 4, softKnee: 0.5 },
3162
+ ssao: { enabled: false, radius: 0.5, intensity: 1, bias: 0.025, samples: 16 },
3163
+ colorGrading: {
3164
+ enabled: false,
3165
+ temperature: 0,
3166
+ tint: 0,
3167
+ saturation: 1,
3168
+ contrast: 1,
3169
+ brightness: 0,
3170
+ gamma: 1
3171
+ },
3172
+ vignette: { enabled: false, intensity: 0.3, smoothness: 0.5, roundness: 1 },
3173
+ chromaticAberration: { enabled: false, intensity: 0.1 },
3174
+ toneMapping: "aces",
3175
+ exposure: 1,
3176
+ antiAliasing: "fxaa"
3177
+ };
3178
+ }
3179
+ var PP_PRESETS = {
3180
+ cinematic: {
3181
+ name: "Cinematic",
3182
+ bloom: { enabled: true, threshold: 0.7, intensity: 0.6, radius: 5, softKnee: 0.5 },
3183
+ colorGrading: {
3184
+ enabled: true,
3185
+ temperature: 10,
3186
+ tint: -5,
3187
+ saturation: 1.1,
3188
+ contrast: 1.15,
3189
+ brightness: -0.05,
3190
+ gamma: 0.95
3191
+ },
3192
+ vignette: { enabled: true, intensity: 0.35, smoothness: 0.6, roundness: 1 },
3193
+ toneMapping: "filmic"
3194
+ },
3195
+ retro: {
3196
+ name: "Retro",
3197
+ colorGrading: {
3198
+ enabled: true,
3199
+ temperature: 30,
3200
+ tint: 10,
3201
+ saturation: 0.7,
3202
+ contrast: 1.3,
3203
+ brightness: -0.1,
3204
+ gamma: 1.1
3205
+ },
3206
+ chromaticAberration: { enabled: true, intensity: 0.3 },
3207
+ vignette: { enabled: true, intensity: 0.5, smoothness: 0.4, roundness: 0.8 },
3208
+ toneMapping: "reinhard"
3209
+ },
3210
+ sciFi: {
3211
+ name: "Sci-Fi",
3212
+ bloom: { enabled: true, threshold: 0.5, intensity: 1, radius: 8, softKnee: 0.3 },
3213
+ ssao: { enabled: true, radius: 0.3, intensity: 1.5, bias: 0.02, samples: 32 },
3214
+ colorGrading: {
3215
+ enabled: true,
3216
+ temperature: -20,
3217
+ tint: 0,
3218
+ saturation: 0.9,
3219
+ contrast: 1.2,
3220
+ brightness: 0,
3221
+ gamma: 0.9
3222
+ },
3223
+ toneMapping: "aces"
3224
+ }
3225
+ };
3226
+ var PostProcessingStack = class {
3227
+ profiles = /* @__PURE__ */ new Map();
3228
+ activeProfileId = null;
3229
+ constructor() {
3230
+ this.profiles.set("default", createDefaultProfile("default", "Default"));
3231
+ }
3232
+ // ---------------------------------------------------------------------------
3233
+ // Profile Management
3234
+ // ---------------------------------------------------------------------------
3235
+ createProfile(id, name) {
3236
+ const profile = createDefaultProfile(id, name);
3237
+ this.profiles.set(id, profile);
3238
+ return profile;
3239
+ }
3240
+ loadPreset(presetName, id) {
3241
+ const preset = PP_PRESETS[presetName];
3242
+ if (!preset) return null;
3243
+ const profileId = id ?? presetName;
3244
+ const profile = {
3245
+ ...createDefaultProfile(profileId, preset.name ?? presetName),
3246
+ ...preset,
3247
+ id: profileId
3248
+ };
3249
+ this.profiles.set(profileId, profile);
3250
+ return profile;
3251
+ }
3252
+ getProfile(id) {
3253
+ return this.profiles.get(id);
3254
+ }
3255
+ getProfileCount() {
3256
+ return this.profiles.size;
3257
+ }
3258
+ removeProfile(id) {
3259
+ if (this.activeProfileId === id) this.activeProfileId = null;
3260
+ return this.profiles.delete(id);
3261
+ }
3262
+ // ---------------------------------------------------------------------------
3263
+ // Active Profile
3264
+ // ---------------------------------------------------------------------------
3265
+ setActive(profileId) {
3266
+ if (!this.profiles.has(profileId)) return false;
3267
+ this.activeProfileId = profileId;
3268
+ return true;
3269
+ }
3270
+ getActive() {
3271
+ if (!this.activeProfileId) return null;
3272
+ return this.profiles.get(this.activeProfileId) ?? null;
3273
+ }
3274
+ // ---------------------------------------------------------------------------
3275
+ // Effect Toggle
3276
+ // ---------------------------------------------------------------------------
3277
+ setEffectEnabled(profileId, effect, enabled) {
3278
+ const profile = this.profiles.get(profileId);
3279
+ if (!profile) return false;
3280
+ profile[effect].enabled = enabled;
3281
+ return true;
3282
+ }
3283
+ // ---------------------------------------------------------------------------
3284
+ // Blending
3285
+ // ---------------------------------------------------------------------------
3286
+ /**
3287
+ * Blend between two profiles by factor t (0 = from, 1 = to).
3288
+ * Useful for smooth transitions between environments.
3289
+ */
3290
+ blendProfiles(fromId, toId, t) {
3291
+ const from = this.profiles.get(fromId);
3292
+ const to = this.profiles.get(toId);
3293
+ if (!from || !to) return null;
3294
+ const lerp = (a, b) => a + (b - a) * t;
3295
+ return {
3296
+ id: `blend_${fromId}_${toId}`,
3297
+ name: `Blend`,
3298
+ bloom: {
3299
+ enabled: t > 0.5 ? to.bloom.enabled : from.bloom.enabled,
3300
+ threshold: lerp(from.bloom.threshold, to.bloom.threshold),
3301
+ intensity: lerp(from.bloom.intensity, to.bloom.intensity),
3302
+ radius: lerp(from.bloom.radius, to.bloom.radius),
3303
+ softKnee: lerp(from.bloom.softKnee, to.bloom.softKnee)
3304
+ },
3305
+ ssao: {
3306
+ enabled: t > 0.5 ? to.ssao.enabled : from.ssao.enabled,
3307
+ radius: lerp(from.ssao.radius, to.ssao.radius),
3308
+ intensity: lerp(from.ssao.intensity, to.ssao.intensity),
3309
+ bias: lerp(from.ssao.bias, to.ssao.bias),
3310
+ samples: Math.round(lerp(from.ssao.samples, to.ssao.samples))
3311
+ },
3312
+ colorGrading: {
3313
+ enabled: t > 0.5 ? to.colorGrading.enabled : from.colorGrading.enabled,
3314
+ temperature: lerp(from.colorGrading.temperature, to.colorGrading.temperature),
3315
+ tint: lerp(from.colorGrading.tint, to.colorGrading.tint),
3316
+ saturation: lerp(from.colorGrading.saturation, to.colorGrading.saturation),
3317
+ contrast: lerp(from.colorGrading.contrast, to.colorGrading.contrast),
3318
+ brightness: lerp(from.colorGrading.brightness, to.colorGrading.brightness),
3319
+ gamma: lerp(from.colorGrading.gamma, to.colorGrading.gamma)
3320
+ },
3321
+ vignette: {
3322
+ enabled: t > 0.5 ? to.vignette.enabled : from.vignette.enabled,
3323
+ intensity: lerp(from.vignette.intensity, to.vignette.intensity),
3324
+ smoothness: lerp(from.vignette.smoothness, to.vignette.smoothness),
3325
+ roundness: lerp(from.vignette.roundness, to.vignette.roundness)
3326
+ },
3327
+ chromaticAberration: {
3328
+ enabled: t > 0.5 ? to.chromaticAberration.enabled : from.chromaticAberration.enabled,
3329
+ intensity: lerp(from.chromaticAberration.intensity, to.chromaticAberration.intensity)
3330
+ },
3331
+ toneMapping: t > 0.5 ? to.toneMapping : from.toneMapping,
3332
+ exposure: lerp(from.exposure, to.exposure),
3333
+ antiAliasing: t > 0.5 ? to.antiAliasing : from.antiAliasing
3334
+ };
3335
+ }
3336
+ };
3337
+
3338
+ // src/rendering/PostProcessStack.ts
3339
+ var _effectId = 0;
3340
+ var PostProcessStack = class {
3341
+ effects = /* @__PURE__ */ new Map();
3342
+ sortedCache = [];
3343
+ dirty = true;
3344
+ enabled = true;
3345
+ // ---------------------------------------------------------------------------
3346
+ // Effect Management
3347
+ // ---------------------------------------------------------------------------
3348
+ addEffect(name, priority, process, params) {
3349
+ const id = `ppfx_${_effectId++}`;
3350
+ const effect = {
3351
+ id,
3352
+ name,
3353
+ priority,
3354
+ enabled: true,
3355
+ weight: 1,
3356
+ params: new Map(Object.entries(params ?? {})),
3357
+ process
3358
+ };
3359
+ this.effects.set(id, effect);
3360
+ this.dirty = true;
3361
+ return effect;
3362
+ }
3363
+ removeEffect(id) {
3364
+ this.dirty = true;
3365
+ return this.effects.delete(id);
3366
+ }
3367
+ // ---------------------------------------------------------------------------
3368
+ // Enable / Disable
3369
+ // ---------------------------------------------------------------------------
3370
+ setEnabled(id, enabled) {
3371
+ const effect = this.effects.get(id);
3372
+ if (effect) effect.enabled = enabled;
3373
+ }
3374
+ setGlobalEnabled(enabled) {
3375
+ this.enabled = enabled;
3376
+ }
3377
+ isGlobalEnabled() {
3378
+ return this.enabled;
3379
+ }
3380
+ setWeight(id, weight) {
3381
+ const effect = this.effects.get(id);
3382
+ if (effect) effect.weight = Math.max(0, Math.min(1, weight));
3383
+ }
3384
+ // ---------------------------------------------------------------------------
3385
+ // Processing
3386
+ // ---------------------------------------------------------------------------
3387
+ process(input, width, height) {
3388
+ if (!this.enabled) return input;
3389
+ const sorted = this.getSorted();
3390
+ let buffer = input;
3391
+ for (const effect of sorted) {
3392
+ if (!effect.enabled || effect.weight <= 0) continue;
3393
+ const processed = effect.process(buffer, width, height);
3394
+ if (effect.weight >= 1) {
3395
+ buffer = processed;
3396
+ } else {
3397
+ const blended = new Float32Array(buffer.length);
3398
+ for (let i = 0; i < buffer.length; i++) {
3399
+ blended[i] = buffer[i] * (1 - effect.weight) + processed[i] * effect.weight;
3400
+ }
3401
+ buffer = blended;
3402
+ }
3403
+ }
3404
+ return buffer;
3405
+ }
3406
+ // ---------------------------------------------------------------------------
3407
+ // Ordering
3408
+ // ---------------------------------------------------------------------------
3409
+ getSorted() {
3410
+ if (this.dirty) {
3411
+ this.sortedCache = [...this.effects.values()].sort((a, b) => a.priority - b.priority);
3412
+ this.dirty = false;
3413
+ }
3414
+ return this.sortedCache;
3415
+ }
3416
+ reorder(id, newPriority) {
3417
+ const effect = this.effects.get(id);
3418
+ if (effect) {
3419
+ effect.priority = newPriority;
3420
+ this.dirty = true;
3421
+ }
3422
+ }
3423
+ // ---------------------------------------------------------------------------
3424
+ // Queries
3425
+ // ---------------------------------------------------------------------------
3426
+ getEffect(id) {
3427
+ return this.effects.get(id);
3428
+ }
3429
+ getEffectCount() {
3430
+ return this.effects.size;
3431
+ }
3432
+ getActiveCount() {
3433
+ return [...this.effects.values()].filter((e) => e.enabled).length;
3434
+ }
3435
+ getEffectNames() {
3436
+ return this.getSorted().map((e) => e.name);
3437
+ }
3438
+ setParam(id, param, value) {
3439
+ this.effects.get(id)?.params.set(param, value);
3440
+ }
3441
+ getParam(id, param) {
3442
+ return this.effects.get(id)?.params.get(param);
3443
+ }
3444
+ };
3445
+ // Annotate the CommonJS export names for ESM import in node:
3446
+ 0 && (module.exports = {
3447
+ BLIT_SHADER,
3448
+ BLOOM_SHADER,
3449
+ CAUSTICS_SHADER,
3450
+ CHROMATIC_ABERRATION_SHADER,
3451
+ COLOR_GRADE_SHADER,
3452
+ CausticsEffect,
3453
+ ChromaticAberrationEffect,
3454
+ DEFAULT_PARAMS,
3455
+ DEFAULT_PIPELINE_CONFIG,
3456
+ DOF_SHADER,
3457
+ FILM_GRAIN_SHADER,
3458
+ FOG_SHADER,
3459
+ FULLSCREEN_VERTEX_SHADER,
3460
+ FXAAEffect,
3461
+ FXAA_SHADER,
3462
+ FilmGrainEffect,
3463
+ MOTION_BLUR_SHADER,
3464
+ PP_PRESETS,
3465
+ PostProcessEffect,
3466
+ PostProcessPipeline,
3467
+ PostProcessStack,
3468
+ PostProcessingStack,
3469
+ SHADER_UTILS,
3470
+ SHARPEN_SHADER,
3471
+ SSAOEffect,
3472
+ SSAO_SHADER,
3473
+ SSGIEffect,
3474
+ SSGI_SHADER,
3475
+ SSREffect,
3476
+ SSR_SHADER,
3477
+ SharpenEffect,
3478
+ TONEMAP_SHADER,
3479
+ ToneMapEffect,
3480
+ UNIFORM_SIZES,
3481
+ VIGNETTE_SHADER,
3482
+ VignetteEffect,
3483
+ buildEffectShader,
3484
+ createEffect,
3485
+ createHDRPipeline,
3486
+ createLDRPipeline,
3487
+ createPostProcessPipeline,
3488
+ getDefaultParams,
3489
+ mergeParams,
3490
+ validateParams
3491
+ });