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