@basementstudio/shader-lab 0.1.0

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 (261) hide show
  1. package/.biome/plugins/README.md +21 -0
  2. package/.biome/plugins/no-anchor-element.grit +12 -0
  3. package/.biome/plugins/no-relative-parent-imports.grit +10 -0
  4. package/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  5. package/.changeset/README.md +17 -0
  6. package/.changeset/config.json +11 -0
  7. package/.editorconfig +40 -0
  8. package/.env.example +81 -0
  9. package/.gitattributes +19 -0
  10. package/.github/workflows/canary.yml +80 -0
  11. package/.github/workflows/ci.yml +37 -0
  12. package/.github/workflows/release.yml +56 -0
  13. package/.tldrignore +84 -0
  14. package/.vscode/extensions.json +20 -0
  15. package/.vscode/settings.json +105 -0
  16. package/README.md +119 -0
  17. package/biome.json +249 -0
  18. package/bun.lock +1224 -0
  19. package/next.config.ts +131 -0
  20. package/package.json +73 -0
  21. package/packages/shader-lab-react/CHANGELOG.md +9 -0
  22. package/packages/shader-lab-react/README.md +119 -0
  23. package/packages/shader-lab-react/assets/patterns/bars/1.svg +3 -0
  24. package/packages/shader-lab-react/assets/patterns/bars/2.svg +3 -0
  25. package/packages/shader-lab-react/assets/patterns/bars/3.svg +3 -0
  26. package/packages/shader-lab-react/assets/patterns/bars/4.svg +3 -0
  27. package/packages/shader-lab-react/assets/patterns/bars/5.svg +3 -0
  28. package/packages/shader-lab-react/assets/patterns/bars/6.svg +3 -0
  29. package/packages/shader-lab-react/assets/patterns/candles/1.svg +3 -0
  30. package/packages/shader-lab-react/assets/patterns/candles/2.svg +3 -0
  31. package/packages/shader-lab-react/assets/patterns/candles/3.svg +3 -0
  32. package/packages/shader-lab-react/assets/patterns/candles/4.svg +3 -0
  33. package/packages/shader-lab-react/assets/patterns/shapes/1.svg +3 -0
  34. package/packages/shader-lab-react/assets/patterns/shapes/2.svg +3 -0
  35. package/packages/shader-lab-react/assets/patterns/shapes/3.svg +3 -0
  36. package/packages/shader-lab-react/assets/patterns/shapes/4.svg +4 -0
  37. package/packages/shader-lab-react/assets/patterns/shapes/5.svg +3 -0
  38. package/packages/shader-lab-react/assets/patterns/shapes/6.svg +4 -0
  39. package/packages/shader-lab-react/assets/textures/blue-noise.png +0 -0
  40. package/packages/shader-lab-react/package.json +36 -0
  41. package/packages/shader-lab-react/scripts/fix-esm-specifiers.mjs +57 -0
  42. package/packages/shader-lab-react/scripts/prepare-dist.mjs +4 -0
  43. package/packages/shader-lab-react/src/ambient/three-tsl.d.ts +146 -0
  44. package/packages/shader-lab-react/src/ambient/three-webgpu.d.ts +51 -0
  45. package/packages/shader-lab-react/src/easings.ts +4 -0
  46. package/packages/shader-lab-react/src/index.ts +35 -0
  47. package/packages/shader-lab-react/src/lib/editor/custom-shader/shared.ts +2 -0
  48. package/packages/shader-lab-react/src/renderer/ascii-atlas.ts +83 -0
  49. package/packages/shader-lab-react/src/renderer/ascii-pass.ts +416 -0
  50. package/packages/shader-lab-react/src/renderer/asset-url.ts +3 -0
  51. package/packages/shader-lab-react/src/renderer/blend-modes.ts +229 -0
  52. package/packages/shader-lab-react/src/renderer/contracts.ts +54 -0
  53. package/packages/shader-lab-react/src/renderer/create-webgpu-renderer.ts +48 -0
  54. package/packages/shader-lab-react/src/renderer/crt-pass.ts +1040 -0
  55. package/packages/shader-lab-react/src/renderer/custom-shader-pass.ts +108 -0
  56. package/packages/shader-lab-react/src/renderer/custom-shader-runtime.ts +309 -0
  57. package/packages/shader-lab-react/src/renderer/dither-textures.ts +99 -0
  58. package/packages/shader-lab-react/src/renderer/dithering-pass.ts +322 -0
  59. package/packages/shader-lab-react/src/renderer/gradient-pass.ts +521 -0
  60. package/packages/shader-lab-react/src/renderer/halftone-pass.ts +932 -0
  61. package/packages/shader-lab-react/src/renderer/ink-pass.ts +802 -0
  62. package/packages/shader-lab-react/src/renderer/live-pass.ts +194 -0
  63. package/packages/shader-lab-react/src/renderer/media-pass.ts +187 -0
  64. package/packages/shader-lab-react/src/renderer/media-texture.ts +66 -0
  65. package/packages/shader-lab-react/src/renderer/particle-grid-pass.ts +389 -0
  66. package/packages/shader-lab-react/src/renderer/pass-node.ts +209 -0
  67. package/packages/shader-lab-react/src/renderer/pattern-atlas.ts +133 -0
  68. package/packages/shader-lab-react/src/renderer/pattern-pass.ts +552 -0
  69. package/packages/shader-lab-react/src/renderer/pipeline-manager.ts +369 -0
  70. package/packages/shader-lab-react/src/renderer/pixel-sorting-pass.ts +277 -0
  71. package/packages/shader-lab-react/src/renderer/shaders/tsl/color/tonemapping.ts +87 -0
  72. package/packages/shader-lab-react/src/renderer/shaders/tsl/cosine-palette.ts +9 -0
  73. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/common.ts +31 -0
  74. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/curl-noise-3d.ts +36 -0
  75. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/curl-noise-4d.ts +36 -0
  76. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/fbm.ts +13 -0
  77. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/perlin-noise-3d.ts +96 -0
  78. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/ridge-noise.ts +24 -0
  79. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/simplex-noise-3d.ts +79 -0
  80. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/simplex-noise-4d.ts +89 -0
  81. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/turbulence.ts +56 -0
  82. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/value-noise-3d.ts +32 -0
  83. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/voronoi-noise-3d.ts +60 -0
  84. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/bloom-edge-pattern.ts +15 -0
  85. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/bloom.ts +11 -0
  86. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/canvas-weave-pattern.ts +24 -0
  87. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/grain-texture-pattern.ts +9 -0
  88. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/repeating-pattern.ts +11 -0
  89. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/atan2.ts +9 -0
  90. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-conj.ts +9 -0
  91. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-cos.ts +10 -0
  92. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-div.ts +11 -0
  93. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-log.ts +7 -0
  94. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-mobius.ts +12 -0
  95. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-mul.ts +9 -0
  96. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-pow.ts +16 -0
  97. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-sin.ts +10 -0
  98. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-sqrt.ts +18 -0
  99. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-tan.ts +12 -0
  100. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-to-polar.ts +10 -0
  101. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/hyperbolic.ts +20 -0
  102. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/index.ts +48 -0
  103. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/rotate.ts +15 -0
  104. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/screen-aspect-uv.ts +15 -0
  105. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-box-2d.ts +6 -0
  106. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-diamond.ts +6 -0
  107. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-rhombus.ts +27 -0
  108. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-sphere.ts +6 -0
  109. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/smax.ts +7 -0
  110. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/smin.ts +7 -0
  111. package/packages/shader-lab-react/src/renderer/text-pass.ts +176 -0
  112. package/packages/shader-lab-react/src/runtime-clock.ts +42 -0
  113. package/packages/shader-lab-react/src/runtime-frame.ts +29 -0
  114. package/packages/shader-lab-react/src/shader-lab-composition.tsx +163 -0
  115. package/packages/shader-lab-react/src/timeline.ts +283 -0
  116. package/packages/shader-lab-react/src/types/editor.ts +5 -0
  117. package/packages/shader-lab-react/src/types.ts +141 -0
  118. package/packages/shader-lab-react/tsconfig.build.json +8 -0
  119. package/packages/shader-lab-react/tsconfig.json +21 -0
  120. package/postcss.config.mjs +5 -0
  121. package/public/assets/fonts/msdf/geist-mono/GeistMono-Regular-msdf-atlas.png +0 -0
  122. package/public/assets/fonts/msdf/geist-mono/GeistMono-Regular-msdf.json +1412 -0
  123. package/public/assets/patterns/bars/1.svg +3 -0
  124. package/public/assets/patterns/bars/2.svg +3 -0
  125. package/public/assets/patterns/bars/3.svg +3 -0
  126. package/public/assets/patterns/bars/4.svg +3 -0
  127. package/public/assets/patterns/bars/5.svg +3 -0
  128. package/public/assets/patterns/bars/6.svg +3 -0
  129. package/public/assets/patterns/candles/1.svg +3 -0
  130. package/public/assets/patterns/candles/2.svg +3 -0
  131. package/public/assets/patterns/candles/3.svg +3 -0
  132. package/public/assets/patterns/candles/4.svg +3 -0
  133. package/public/assets/patterns/shapes/1.svg +3 -0
  134. package/public/assets/patterns/shapes/2.svg +3 -0
  135. package/public/assets/patterns/shapes/3.svg +3 -0
  136. package/public/assets/patterns/shapes/4.svg +4 -0
  137. package/public/assets/patterns/shapes/5.svg +3 -0
  138. package/public/assets/patterns/shapes/6.svg +4 -0
  139. package/public/fonts/geist/Geist-Mono.woff2 +0 -0
  140. package/public/textures/blue-noise.png +0 -0
  141. package/public/textures/crt-mask.png +0 -0
  142. package/src/app/design/page.tsx +398 -0
  143. package/src/app/favicon.ico +0 -0
  144. package/src/app/globals.css +280 -0
  145. package/src/app/layout.tsx +89 -0
  146. package/src/app/page.tsx +20 -0
  147. package/src/app/robots.ts +13 -0
  148. package/src/app/sitemap.ts +13 -0
  149. package/src/components/editor/editor-canvas-viewport.tsx +116 -0
  150. package/src/components/editor/editor-export-dialog.tsx +1177 -0
  151. package/src/components/editor/editor-timeline-overlay.tsx +983 -0
  152. package/src/components/editor/editor-topbar.tsx +287 -0
  153. package/src/components/editor/layer-sidebar.tsx +738 -0
  154. package/src/components/editor/properties-sidebar-content.tsx +574 -0
  155. package/src/components/editor/properties-sidebar-fields.tsx +389 -0
  156. package/src/components/editor/properties-sidebar-utils.ts +178 -0
  157. package/src/components/editor/properties-sidebar.tsx +421 -0
  158. package/src/components/ui/button/index.tsx +57 -0
  159. package/src/components/ui/color-picker/index.tsx +358 -0
  160. package/src/components/ui/glass-panel/index.tsx +45 -0
  161. package/src/components/ui/icon-button/index.tsx +46 -0
  162. package/src/components/ui/select/index.tsx +136 -0
  163. package/src/components/ui/slider/index.tsx +192 -0
  164. package/src/components/ui/toggle/index.tsx +34 -0
  165. package/src/components/ui/typography/index.tsx +61 -0
  166. package/src/components/ui/xy-pad/index.tsx +160 -0
  167. package/src/features/editor/components/editor-export-dialog.module.css +271 -0
  168. package/src/hooks/use-editor-renderer.ts +182 -0
  169. package/src/lib/app.ts +6 -0
  170. package/src/lib/cn.ts +7 -0
  171. package/src/lib/easings.ts +240 -0
  172. package/src/lib/editor/config/layer-registry.ts +2434 -0
  173. package/src/lib/editor/custom-shader/shared.ts +28 -0
  174. package/src/lib/editor/export.ts +420 -0
  175. package/src/lib/editor/history.ts +71 -0
  176. package/src/lib/editor/layers.ts +76 -0
  177. package/src/lib/editor/parameter-schema.ts +75 -0
  178. package/src/lib/editor/project-file.ts +145 -0
  179. package/src/lib/editor/shader-export-snippet.ts +37 -0
  180. package/src/lib/editor/shader-export.ts +315 -0
  181. package/src/lib/editor/timeline/evaluate.ts +252 -0
  182. package/src/lib/editor/view-transform.ts +58 -0
  183. package/src/lib/fonts.ts +28 -0
  184. package/src/renderer/ascii-atlas.ts +83 -0
  185. package/src/renderer/ascii-pass.ts +416 -0
  186. package/src/renderer/blend-modes.ts +229 -0
  187. package/src/renderer/contracts.ts +161 -0
  188. package/src/renderer/create-webgpu-renderer.ts +48 -0
  189. package/src/renderer/crt-pass.ts +1040 -0
  190. package/src/renderer/custom-shader-pass.ts +117 -0
  191. package/src/renderer/custom-shader-runtime.ts +309 -0
  192. package/src/renderer/dither-textures.ts +99 -0
  193. package/src/renderer/dithering-pass.ts +322 -0
  194. package/src/renderer/gradient-pass.ts +520 -0
  195. package/src/renderer/halftone-pass.ts +932 -0
  196. package/src/renderer/ink-pass.ts +683 -0
  197. package/src/renderer/live-pass.ts +194 -0
  198. package/src/renderer/media-pass.ts +187 -0
  199. package/src/renderer/media-texture.ts +66 -0
  200. package/src/renderer/particle-grid-pass.ts +389 -0
  201. package/src/renderer/pass-node-factory.ts +33 -0
  202. package/src/renderer/pass-node.ts +209 -0
  203. package/src/renderer/pattern-atlas.ts +97 -0
  204. package/src/renderer/pattern-pass.ts +552 -0
  205. package/src/renderer/pipeline-manager.ts +343 -0
  206. package/src/renderer/pixel-sorting-pass.ts +277 -0
  207. package/src/renderer/project-clock.ts +57 -0
  208. package/src/renderer/shaders/tsl/color/tonemapping.ts +86 -0
  209. package/src/renderer/shaders/tsl/cosine-palette.ts +8 -0
  210. package/src/renderer/shaders/tsl/noise/common.ts +30 -0
  211. package/src/renderer/shaders/tsl/noise/curl-noise-3d.ts +35 -0
  212. package/src/renderer/shaders/tsl/noise/curl-noise-4d.ts +35 -0
  213. package/src/renderer/shaders/tsl/noise/fbm.ts +12 -0
  214. package/src/renderer/shaders/tsl/noise/perlin-noise-3d.ts +97 -0
  215. package/src/renderer/shaders/tsl/noise/ridge-noise.ts +23 -0
  216. package/src/renderer/shaders/tsl/noise/simplex-noise-3d.ts +78 -0
  217. package/src/renderer/shaders/tsl/noise/simplex-noise-4d.ts +88 -0
  218. package/src/renderer/shaders/tsl/noise/turbulence.ts +55 -0
  219. package/src/renderer/shaders/tsl/noise/value-noise-3d.ts +31 -0
  220. package/src/renderer/shaders/tsl/noise/voronoi-noise-3d.ts +59 -0
  221. package/src/renderer/shaders/tsl/patterns/bloom-edge-pattern.ts +14 -0
  222. package/src/renderer/shaders/tsl/patterns/bloom.ts +10 -0
  223. package/src/renderer/shaders/tsl/patterns/canvas-weave-pattern.ts +23 -0
  224. package/src/renderer/shaders/tsl/patterns/grain-texture-pattern.ts +8 -0
  225. package/src/renderer/shaders/tsl/patterns/repeating-pattern.ts +10 -0
  226. package/src/renderer/shaders/tsl/utils/atan2.ts +8 -0
  227. package/src/renderer/shaders/tsl/utils/complex-conj.ts +8 -0
  228. package/src/renderer/shaders/tsl/utils/complex-cos.ts +9 -0
  229. package/src/renderer/shaders/tsl/utils/complex-div.ts +10 -0
  230. package/src/renderer/shaders/tsl/utils/complex-log.ts +6 -0
  231. package/src/renderer/shaders/tsl/utils/complex-mobius.ts +11 -0
  232. package/src/renderer/shaders/tsl/utils/complex-mul.ts +8 -0
  233. package/src/renderer/shaders/tsl/utils/complex-pow.ts +15 -0
  234. package/src/renderer/shaders/tsl/utils/complex-sin.ts +9 -0
  235. package/src/renderer/shaders/tsl/utils/complex-sqrt.ts +17 -0
  236. package/src/renderer/shaders/tsl/utils/complex-tan.ts +11 -0
  237. package/src/renderer/shaders/tsl/utils/complex-to-polar.ts +9 -0
  238. package/src/renderer/shaders/tsl/utils/hyperbolic.ts +19 -0
  239. package/src/renderer/shaders/tsl/utils/index.ts +47 -0
  240. package/src/renderer/shaders/tsl/utils/rotate.ts +14 -0
  241. package/src/renderer/shaders/tsl/utils/screen-aspect-uv.ts +14 -0
  242. package/src/renderer/shaders/tsl/utils/sd-box-2d.ts +5 -0
  243. package/src/renderer/shaders/tsl/utils/sd-diamond.ts +5 -0
  244. package/src/renderer/shaders/tsl/utils/sd-rhombus.ts +26 -0
  245. package/src/renderer/shaders/tsl/utils/sd-sphere.ts +5 -0
  246. package/src/renderer/shaders/tsl/utils/smax.ts +7 -0
  247. package/src/renderer/shaders/tsl/utils/smin.ts +6 -0
  248. package/src/renderer/text-pass.ts +176 -0
  249. package/src/store/asset-store.ts +193 -0
  250. package/src/store/editor-store.ts +223 -0
  251. package/src/store/history-store.ts +172 -0
  252. package/src/store/index.ts +31 -0
  253. package/src/store/layer-store.ts +675 -0
  254. package/src/store/timeline-store.ts +572 -0
  255. package/src/types/assets.d.ts +6 -0
  256. package/src/types/css.d.ts +21 -0
  257. package/src/types/editor.ts +357 -0
  258. package/src/types/react.d.ts +15 -0
  259. package/src/types/three-tsl.d.ts +146 -0
  260. package/src/types/three-webgpu.d.ts +51 -0
  261. package/tsconfig.json +49 -0
@@ -0,0 +1,683 @@
1
+ import * as THREE from "three/webgpu"
2
+ import { bloom } from "three/examples/jsm/tsl/display/BloomNode.js"
3
+ import {
4
+ clamp,
5
+ float,
6
+ max,
7
+ mix,
8
+ pow,
9
+ select,
10
+ sin,
11
+ smoothstep,
12
+ texture as tslTexture,
13
+ type TSLNode,
14
+ uniform,
15
+ uv,
16
+ vec2,
17
+ vec3,
18
+ vec4,
19
+ } from "three/tsl"
20
+ import { PassNode } from "@/renderer/pass-node"
21
+ import { loadImageTexture } from "@/renderer/media-texture"
22
+ import { grainTexturePattern } from "@/renderer/shaders/tsl/patterns/grain-texture-pattern"
23
+ import type { LayerParameterValues } from "@/types/editor"
24
+
25
+ type Node = TSLNode
26
+
27
+ const INTERNAL_TARGET_OPTIONS = {
28
+ depthBuffer: false,
29
+ format: THREE.RGBAFormat,
30
+ generateMipmaps: false,
31
+ magFilter: THREE.LinearFilter,
32
+ minFilter: THREE.LinearFilter,
33
+ stencilBuffer: false,
34
+ type: THREE.HalfFloatType,
35
+ } as const
36
+
37
+ function clamp01(value: number): number {
38
+ return Math.max(0, Math.min(1, value))
39
+ }
40
+
41
+ function hexToRgb(hex: string): [number, number, number] {
42
+ const normalized = hex.trim().replace("#", "")
43
+ const value =
44
+ normalized.length === 3
45
+ ? normalized
46
+ .split("")
47
+ .map((entry) => `${entry}${entry}`)
48
+ .join("")
49
+ : normalized.padEnd(6, "0").slice(0, 6)
50
+
51
+ return [
52
+ Number.parseInt(value.slice(0, 2), 16) / 255,
53
+ Number.parseInt(value.slice(2, 4), 16) / 255,
54
+ Number.parseInt(value.slice(4, 6), 16) / 255,
55
+ ]
56
+ }
57
+
58
+ export class InkPass extends PassNode {
59
+ private readonly blurScene: THREE.Scene
60
+ private readonly compositeScene: THREE.Scene
61
+ private readonly copyScene: THREE.Scene
62
+ private readonly orthoCamera: THREE.OrthographicCamera
63
+ private readonly blurMaterial: THREE.MeshBasicNodeMaterial
64
+ private readonly compositeMaterial: THREE.MeshBasicNodeMaterial
65
+ private readonly copyMaterial: THREE.MeshBasicNodeMaterial
66
+ private readonly blurInputNode: Node
67
+ private readonly crispInputNode: Node
68
+ private readonly finalInputNode: Node
69
+ private readonly copyInputNode: Node
70
+ private readonly noiseInputNode: Node
71
+
72
+ private blurSampleNodes: Node[] = []
73
+ private compositeBlurNodes: Node[] = []
74
+ private noiseSampleNodes: Node[] = []
75
+
76
+ private bloomEnabled = false
77
+ private bloomNode: ReturnType<typeof bloom> | null = null
78
+ private readonly bloomIntensityUniform: Node
79
+ private readonly bloomRadiusUniform: Node
80
+ private readonly bloomSoftnessUniform: Node
81
+ private readonly bloomThresholdUniform: Node
82
+
83
+ private readonly backgroundColorUniform: Node
84
+ private readonly coreColorUniform: Node
85
+ private readonly edgeColorUniform: Node
86
+ private readonly midColorUniform: Node
87
+
88
+ private readonly blurStrengthUniform: Node
89
+ private readonly crispBlendUniform: Node
90
+ private readonly directionXUniform: Node
91
+ private readonly directionYUniform: Node
92
+ private readonly dripLengthUniform: Node
93
+ private readonly dripWeightUniform: Node
94
+ private readonly fluidNoiseUniform: Node
95
+ private readonly grainEnabledUniform: Node
96
+ private readonly grainIntensityUniform: Node
97
+ private readonly grainScaleUniform: Node
98
+ private readonly blurSpreadUniform: Node
99
+ private readonly noiseScaleUniform: Node
100
+ private readonly passIndexUniform: Node
101
+ private readonly resolutionWidthUniform: Node
102
+ private readonly resolutionHeightUniform: Node
103
+ private readonly smokeSpeedUniform: Node
104
+ private readonly smokeTurbulenceUniform: Node
105
+ private readonly timeUniform: Node
106
+
107
+ private blurPassCount = 12
108
+ private crispPassCount = 3
109
+ private compositeTarget: THREE.WebGLRenderTarget
110
+ private crispTarget: THREE.WebGLRenderTarget
111
+ private readTarget: THREE.WebGLRenderTarget
112
+ private writeTarget: THREE.WebGLRenderTarget
113
+ private readonly placeholder: THREE.Texture
114
+ private noiseTexture: THREE.Texture | null = null
115
+ private noiseLoadStarted = false
116
+ private needsRefresh = true
117
+ private width = 1
118
+ private height = 1
119
+ private isAnimated = true
120
+
121
+ constructor(layerId: string) {
122
+ super(layerId)
123
+
124
+ this.orthoCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
125
+ this.blurScene = new THREE.Scene()
126
+ this.compositeScene = new THREE.Scene()
127
+ this.copyScene = new THREE.Scene()
128
+
129
+ this.placeholder = new THREE.Texture()
130
+ const flippedUv = vec2(uv().x, float(1).sub(uv().y))
131
+ this.blurInputNode = tslTexture(this.placeholder, flippedUv)
132
+ this.crispInputNode = tslTexture(this.placeholder, flippedUv)
133
+ this.finalInputNode = tslTexture(this.placeholder, flippedUv)
134
+ this.copyInputNode = tslTexture(this.placeholder, flippedUv)
135
+ this.noiseInputNode = tslTexture(this.placeholder, flippedUv)
136
+
137
+ this.bloomIntensityUniform = uniform(1.25)
138
+ this.bloomRadiusUniform = uniform(6)
139
+ this.bloomSoftnessUniform = uniform(0.35)
140
+ this.bloomThresholdUniform = uniform(0.6)
141
+
142
+ this.backgroundColorUniform = uniform(new THREE.Vector3(0.039, 0.043, 0.051))
143
+ this.coreColorUniform = uniform(new THREE.Vector3(1.0, 0.992, 0.91))
144
+ this.midColorUniform = uniform(new THREE.Vector3(0.784, 0.961, 0.259))
145
+ this.edgeColorUniform = uniform(new THREE.Vector3(0.0, 0.788, 0.655))
146
+
147
+ this.blurStrengthUniform = uniform(0.02)
148
+ this.crispBlendUniform = uniform(0.75)
149
+ this.directionXUniform = uniform(0.3746)
150
+ this.directionYUniform = uniform(0.9271)
151
+ this.dripLengthUniform = uniform(7.1)
152
+ this.dripWeightUniform = uniform(1.2)
153
+ this.fluidNoiseUniform = uniform(0.2)
154
+ this.grainEnabledUniform = uniform(1)
155
+ this.grainIntensityUniform = uniform(0.3)
156
+ this.grainScaleUniform = uniform(1.5)
157
+ this.blurSpreadUniform = uniform(1.7)
158
+ this.noiseScaleUniform = uniform(1)
159
+ this.passIndexUniform = uniform(0)
160
+ this.resolutionWidthUniform = uniform(1)
161
+ this.resolutionHeightUniform = uniform(1)
162
+ this.smokeSpeedUniform = uniform(0.2)
163
+ this.smokeTurbulenceUniform = uniform(0.25)
164
+ this.timeUniform = uniform(0)
165
+
166
+ this.readTarget = new THREE.WebGLRenderTarget(1, 1, INTERNAL_TARGET_OPTIONS)
167
+ this.writeTarget = new THREE.WebGLRenderTarget(1, 1, INTERNAL_TARGET_OPTIONS)
168
+ this.crispTarget = new THREE.WebGLRenderTarget(1, 1, INTERNAL_TARGET_OPTIONS)
169
+ this.compositeTarget = new THREE.WebGLRenderTarget(1, 1, INTERNAL_TARGET_OPTIONS)
170
+
171
+ this.blurMaterial = new THREE.MeshBasicNodeMaterial()
172
+ this.blurMaterial.colorNode = this.buildBlurNode()
173
+ const blurMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.blurMaterial)
174
+ blurMesh.frustumCulled = false
175
+ this.blurScene.add(blurMesh)
176
+
177
+ this.copyMaterial = new THREE.MeshBasicNodeMaterial()
178
+ this.copyMaterial.colorNode = vec4(this.copyInputNode.rgb, float(1))
179
+ const copyMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.copyMaterial)
180
+ copyMesh.frustumCulled = false
181
+ this.copyScene.add(copyMesh)
182
+
183
+ this.compositeMaterial = new THREE.MeshBasicNodeMaterial()
184
+ this.compositeMaterial.colorNode = this.buildCompositeNode()
185
+ const compositeMesh = new THREE.Mesh(
186
+ new THREE.PlaneGeometry(2, 2),
187
+ this.compositeMaterial,
188
+ )
189
+ compositeMesh.frustumCulled = false
190
+ this.compositeScene.add(compositeMesh)
191
+
192
+ this.rebuildEffectNode()
193
+ }
194
+
195
+ override render(
196
+ renderer: THREE.WebGPURenderer,
197
+ inputTexture: THREE.Texture,
198
+ outputTarget: THREE.WebGLRenderTarget,
199
+ time: number,
200
+ delta: number,
201
+ ): void {
202
+ this.ensureNoiseTexture()
203
+
204
+ this.blurInputNode.value = inputTexture
205
+ this.noiseInputNode.value = this.noiseTexture ?? this.placeholder
206
+ for (const node of this.blurSampleNodes) {
207
+ node.value = inputTexture
208
+ }
209
+ for (const node of this.noiseSampleNodes) {
210
+ node.value = this.noiseTexture ?? this.placeholder
211
+ }
212
+ this.passIndexUniform.value = 0
213
+
214
+ renderer.setRenderTarget(this.readTarget)
215
+ renderer.render(this.blurScene, this.orthoCamera)
216
+
217
+ if (this.crispPassCount <= 1) {
218
+ this.copyInputNode.value = this.readTarget.texture
219
+ renderer.setRenderTarget(this.crispTarget)
220
+ renderer.render(this.copyScene, this.orthoCamera)
221
+ }
222
+
223
+ const totalPasses = Math.max(this.blurPassCount, this.crispPassCount)
224
+ let readTarget = this.readTarget
225
+ let writeTarget = this.writeTarget
226
+
227
+ for (let passIndex = 1; passIndex < totalPasses; passIndex += 1) {
228
+ this.blurInputNode.value = readTarget.texture
229
+ for (const node of this.blurSampleNodes) {
230
+ node.value = readTarget.texture
231
+ }
232
+ this.passIndexUniform.value = passIndex
233
+ renderer.setRenderTarget(writeTarget)
234
+ renderer.render(this.blurScene, this.orthoCamera)
235
+
236
+ if (passIndex === this.crispPassCount - 1) {
237
+ this.copyInputNode.value = writeTarget.texture
238
+ renderer.setRenderTarget(this.crispTarget)
239
+ renderer.render(this.copyScene, this.orthoCamera)
240
+ }
241
+
242
+ const temp = readTarget
243
+ readTarget = writeTarget
244
+ writeTarget = temp
245
+ }
246
+
247
+ this.finalInputNode.value = readTarget.texture
248
+ this.crispInputNode.value = this.crispTarget.texture
249
+ for (const node of this.compositeBlurNodes) {
250
+ node.value = readTarget.texture
251
+ }
252
+ renderer.setRenderTarget(this.compositeTarget)
253
+ renderer.render(this.compositeScene, this.orthoCamera)
254
+
255
+ this.finalInputNode.value = this.compositeTarget.texture
256
+ super.render(renderer, inputTexture, outputTarget, time, delta)
257
+ this.needsRefresh = false
258
+ }
259
+
260
+ protected override beforeRender(time: number): void {
261
+ this.timeUniform.value = time
262
+ }
263
+
264
+ override needsContinuousRender(): boolean {
265
+ return this.isAnimated || this.needsRefresh
266
+ }
267
+
268
+ override updateParams(params: LayerParameterValues): void {
269
+ const angle = (((typeof params.blurDirection === "number" ? params.blurDirection : 68) *
270
+ Math.PI) /
271
+ 180)
272
+
273
+ this.directionXUniform.value = Math.cos(angle)
274
+ this.directionYUniform.value = Math.sin(angle)
275
+ this.blurPassCount =
276
+ typeof params.blurPasses === "number" ? Math.max(1, Math.round(params.blurPasses)) : 12
277
+ this.crispPassCount =
278
+ typeof params.crispPasses === "number" ? Math.max(1, Math.round(params.crispPasses)) : 3
279
+ this.blurStrengthUniform.value =
280
+ typeof params.blurStrength === "number" ? Math.max(0.001, params.blurStrength) : 0.02
281
+ this.crispBlendUniform.value =
282
+ typeof params.crispBlend === "number" ? clamp01(params.crispBlend) : 0.75
283
+ this.dripLengthUniform.value =
284
+ typeof params.dripLength === "number" ? Math.max(1, params.dripLength) : 7.1
285
+ this.dripWeightUniform.value =
286
+ typeof params.dripWeight === "number" ? Math.max(0.2, params.dripWeight) : 1.2
287
+ this.fluidNoiseUniform.value =
288
+ typeof params.fluidNoise === "number" ? Math.max(0, params.fluidNoise) : 0.2
289
+ this.noiseScaleUniform.value =
290
+ typeof params.noiseScale === "number" ? Math.max(0.5, params.noiseScale) : 1
291
+ this.smokeSpeedUniform.value =
292
+ typeof params.smokeSpeed === "number" ? Math.max(0, params.smokeSpeed) : 0.2
293
+ this.smokeTurbulenceUniform.value =
294
+ typeof params.smokeTurbulence === "number" ? Math.max(0, params.smokeTurbulence) : 0.25
295
+ this.blurSpreadUniform.value =
296
+ typeof params.blurSpread === "number" ? Math.max(0.5, params.blurSpread) : 1.7
297
+ this.grainEnabledUniform.value = params.grainEnabled === false ? 0 : 1
298
+ this.grainIntensityUniform.value =
299
+ typeof params.grainIntensity === "number" ? clamp01(params.grainIntensity) : 0.3
300
+ this.grainScaleUniform.value =
301
+ typeof params.grainScale === "number" ? Math.max(0.5, params.grainScale) : 1.5
302
+
303
+ this.setColorUniform(
304
+ this.backgroundColorUniform,
305
+ typeof params.backgroundColor === "string" ? params.backgroundColor : "#0a0b0d",
306
+ )
307
+ this.setColorUniform(
308
+ this.coreColorUniform,
309
+ typeof params.coreColor === "string" ? params.coreColor : "#fffde8",
310
+ )
311
+ this.setColorUniform(
312
+ this.midColorUniform,
313
+ typeof params.midColor === "string" ? params.midColor : "#c8f542",
314
+ )
315
+ this.setColorUniform(
316
+ this.edgeColorUniform,
317
+ typeof params.edgeColor === "string" ? params.edgeColor : "#00c9a7",
318
+ )
319
+
320
+ const nextBloomEnabled = params.bloomEnabled === true
321
+ const nextBloomIntensity =
322
+ typeof params.bloomIntensity === "number" ? Math.max(0, params.bloomIntensity) : 1.25
323
+ const nextBloomThreshold =
324
+ typeof params.bloomThreshold === "number" ? clamp01(params.bloomThreshold) : 0.6
325
+ const nextBloomRadius =
326
+ typeof params.bloomRadius === "number" ? Math.max(0, params.bloomRadius) : 6
327
+ const nextBloomSoftness =
328
+ typeof params.bloomSoftness === "number" ? clamp01(params.bloomSoftness) : 0.35
329
+
330
+ this.bloomIntensityUniform.value = nextBloomIntensity
331
+ this.bloomRadiusUniform.value = nextBloomRadius
332
+ this.bloomSoftnessUniform.value = nextBloomSoftness
333
+ this.bloomThresholdUniform.value = nextBloomThreshold
334
+
335
+ if (nextBloomEnabled !== this.bloomEnabled) {
336
+ this.bloomEnabled = nextBloomEnabled
337
+ this.rebuildEffectNode()
338
+ } else if (this.bloomNode) {
339
+ this.bloomNode.strength.value = nextBloomIntensity
340
+ this.bloomNode.radius.value = this.normalizeBloomRadius(nextBloomRadius)
341
+ this.bloomNode.threshold.value = nextBloomThreshold
342
+ this.bloomNode.smoothWidth.value = this.normalizeBloomSoftness(nextBloomSoftness)
343
+ }
344
+
345
+ this.isAnimated = (this.smokeSpeedUniform.value as number) > 0
346
+ this.needsRefresh = true
347
+ }
348
+
349
+ override resize(width: number, height: number): void {
350
+ this.width = Math.max(1, width)
351
+ this.height = Math.max(1, height)
352
+ this.resolutionWidthUniform.value = this.width
353
+ this.resolutionHeightUniform.value = this.height
354
+ this.readTarget.setSize(this.width, this.height)
355
+ this.writeTarget.setSize(this.width, this.height)
356
+ this.crispTarget.setSize(this.width, this.height)
357
+ this.compositeTarget.setSize(this.width, this.height)
358
+ this.needsRefresh = true
359
+ }
360
+
361
+ override dispose(): void {
362
+ this.disposeBloomNode()
363
+ this.noiseTexture?.dispose()
364
+ this.readTarget?.dispose()
365
+ this.writeTarget?.dispose()
366
+ this.crispTarget?.dispose()
367
+ this.compositeTarget?.dispose()
368
+ this.blurMaterial?.dispose()
369
+ this.copyMaterial?.dispose()
370
+ this.compositeMaterial?.dispose()
371
+ this.placeholder.dispose()
372
+ super.dispose()
373
+ }
374
+
375
+ protected override buildEffectNode(): Node {
376
+ if (!this.finalInputNode) {
377
+ return vec4(float(0), float(0), float(0), float(1))
378
+ }
379
+
380
+ this.disposeBloomNode()
381
+ this.bloomNode = null
382
+
383
+ const baseColor = vec3(this.finalInputNode.r, this.finalInputNode.g, this.finalInputNode.b)
384
+
385
+ if (!this.bloomEnabled) {
386
+ return vec4(baseColor, float(1))
387
+ }
388
+
389
+ this.bloomNode = bloom(
390
+ vec4(baseColor, float(1)),
391
+ this.bloomIntensityUniform.value as number,
392
+ this.normalizeBloomRadius(this.bloomRadiusUniform.value as number),
393
+ this.bloomThresholdUniform.value as number,
394
+ )
395
+ this.bloomNode.smoothWidth.value = this.normalizeBloomSoftness(
396
+ this.bloomSoftnessUniform.value as number,
397
+ )
398
+
399
+ return vec4(
400
+ clamp(
401
+ baseColor.add(this.getBloomTextureNode().rgb),
402
+ vec3(float(0), float(0), float(0)),
403
+ vec3(float(1), float(1), float(1)),
404
+ ),
405
+ float(1),
406
+ )
407
+ }
408
+
409
+ private buildBlurNode(): Node {
410
+ this.blurSampleNodes = []
411
+ this.noiseSampleNodes = []
412
+
413
+ const texUv = vec2(uv().x, float(1).sub(uv().y))
414
+ const texelSize = vec2(
415
+ float(1).div(this.resolutionWidthUniform),
416
+ float(1).div(this.resolutionHeightUniform),
417
+ )
418
+ const original = this.blurInputNode
419
+ const originalIntensity = max(max(original.r, original.g), original.b)
420
+ const rotatedUv = vec2(
421
+ texUv.x.mul(float(Math.cos(0.7854))).sub(texUv.y.mul(float(Math.sin(0.7854)))),
422
+ texUv.x.mul(float(Math.sin(0.7854))).add(texUv.y.mul(float(Math.cos(0.7854)))),
423
+ )
424
+ const timeOffset1 = vec2(
425
+ this.timeUniform.mul(this.smokeSpeedUniform).mul(0.3),
426
+ this.timeUniform.mul(this.smokeSpeedUniform).mul(0.15),
427
+ )
428
+ const timeOffset2 = vec2(
429
+ sin(this.timeUniform.mul(this.smokeSpeedUniform).mul(0.7)).mul(0.1),
430
+ sin(this.timeUniform.mul(this.smokeSpeedUniform).mul(0.5).add(float(1.5707963))).mul(0.08),
431
+ )
432
+ const noiseUv1 = texUv
433
+ .mul(this.noiseScaleUniform)
434
+ .add(vec2(this.passIndexUniform.mul(0.1), this.passIndexUniform.mul(0.1)))
435
+ .add(timeOffset1)
436
+ const noiseUv2 = rotatedUv
437
+ .mul(this.noiseScaleUniform)
438
+ .mul(0.8)
439
+ .add(vec2(this.passIndexUniform.mul(0.15), this.passIndexUniform.mul(0.15)))
440
+ .add(timeOffset1.mul(1.2))
441
+ const turbulenceUv = rotatedUv
442
+ .mul(this.noiseScaleUniform)
443
+ .mul(0.6)
444
+ .add(timeOffset2.mul(2))
445
+
446
+ const noiseSample1 = this.trackNoiseSampleNode(noiseUv1)
447
+ const noiseSample2 = this.trackNoiseSampleNode(noiseUv2)
448
+ const turbulenceSample = this.trackNoiseSampleNode(turbulenceUv)
449
+ const noiseSample = mix(noiseSample1.rgb, noiseSample2.rgb, float(0.5))
450
+
451
+ const noiseX = noiseSample.r
452
+ .sub(0.5)
453
+ .mul(2)
454
+ .mul(this.fluidNoiseUniform)
455
+ .add(turbulenceSample.r.sub(0.5).mul(this.smokeTurbulenceUniform))
456
+ const noiseY = noiseSample.g
457
+ .sub(0.5)
458
+ .mul(2)
459
+ .mul(this.fluidNoiseUniform)
460
+ .mul(0.4)
461
+ .add(turbulenceSample.g.sub(0.5).mul(this.smokeTurbulenceUniform).mul(0.5))
462
+
463
+ const flowDir = vec2(
464
+ this.directionXUniform.add(noiseX),
465
+ this.directionYUniform.add(noiseY),
466
+ ).normalize()
467
+ const baseNoise = noiseSample.b.sub(0.5).mul(0.03).mul(this.fluidNoiseUniform)
468
+
469
+ let result: Node = vec4(float(0), float(0), float(0), float(0))
470
+ let totalWeight: Node = float(0)
471
+
472
+ for (let sampleIndex = 0; sampleIndex < 10; sampleIndex += 1) {
473
+ const t = float(sampleIndex / 9)
474
+ const asymmetry = smoothstep(float(0), float(1), t).mul(t).add(t.mul(0.5))
475
+ const sampleDist = asymmetry
476
+ .mul(this.dripLengthUniform)
477
+ .mul(this.blurStrengthUniform)
478
+ .mul(float(1).add(this.passIndexUniform.mul(this.blurSpreadUniform).mul(0.15)))
479
+ const sampleNoise = baseNoise.mul(float(1 + sampleIndex * 0.1))
480
+ const disperseTurbulence = sampleNoise.mul(t).mul(this.smokeTurbulenceUniform)
481
+ const samplePos = texUv
482
+ .add(flowDir.mul(sampleDist).mul(texelSize).mul(100))
483
+ .add(
484
+ vec2(
485
+ sampleNoise.add(disperseTurbulence),
486
+ sampleNoise.add(disperseTurbulence).mul(0.3),
487
+ ),
488
+ )
489
+ const weight = mix(float(1).sub(t), float(1), smoothstep(float(0.35), float(0), t))
490
+ const sample = this.trackBlurSampleNode(samplePos)
491
+ result = result.add(sample.mul(weight))
492
+ totalWeight = totalWeight.add(weight)
493
+ }
494
+
495
+ for (let sampleIndex = 1; sampleIndex <= 2; sampleIndex += 1) {
496
+ const t = float(sampleIndex / 3)
497
+ const sampleDist = t
498
+ .mul(this.dripLengthUniform)
499
+ .mul(this.blurStrengthUniform)
500
+ .mul(0.2)
501
+ const samplePos = texUv.sub(flowDir.mul(sampleDist).mul(texelSize).mul(100))
502
+ const weight = float(1).sub(t).mul(0.4)
503
+ const sample = this.trackBlurSampleNode(samplePos)
504
+ result = result.add(sample.mul(weight))
505
+ totalWeight = totalWeight.add(weight)
506
+ }
507
+
508
+ const blurred = result.div(max(totalWeight, float(0.0001)))
509
+ const lifted = max(blurred, original)
510
+ return vec4(mix(blurred.rgb, lifted.rgb, originalIntensity.mul(0.5)), float(1))
511
+ }
512
+
513
+ private buildCompositeNode(): Node {
514
+ this.compositeBlurNodes = []
515
+
516
+ const texUv = vec2(uv().x, float(1).sub(uv().y))
517
+ const blurSample = this.finalInputNode
518
+ const crispSample = this.crispInputNode
519
+ const blurIntensity = max(max(blurSample.r, blurSample.g), blurSample.b)
520
+ const crispIntensity = max(max(crispSample.r, crispSample.g), crispSample.b)
521
+ const texelSize = vec2(
522
+ float(1).div(this.resolutionWidthUniform),
523
+ float(1).div(this.resolutionHeightUniform),
524
+ )
525
+ const blurR = this.trackCompositeBlurNode(
526
+ texUv.add(vec2(texelSize.x.mul(2), texelSize.y)),
527
+ ).r
528
+ const blurB = this.trackCompositeBlurNode(
529
+ texUv.sub(vec2(texelSize.x.mul(2), texelSize.y)),
530
+ ).b
531
+
532
+ const backgroundColor = vec3(
533
+ float(this.backgroundColorUniform.x),
534
+ float(this.backgroundColorUniform.y),
535
+ float(this.backgroundColorUniform.z),
536
+ )
537
+ const edgeColor = vec3(
538
+ float(this.edgeColorUniform.x),
539
+ float(this.edgeColorUniform.y),
540
+ float(this.edgeColorUniform.z),
541
+ )
542
+ const midColor = vec3(
543
+ float(this.midColorUniform.x),
544
+ float(this.midColorUniform.y),
545
+ float(this.midColorUniform.z),
546
+ )
547
+ const coreColor = vec3(
548
+ float(this.coreColorUniform.x),
549
+ float(this.coreColorUniform.y),
550
+ float(this.coreColorUniform.z),
551
+ )
552
+
553
+ const fluidColor = this.applyColorGradient(blurIntensity, backgroundColor, edgeColor, midColor, coreColor)
554
+ const crispColor = this.applyColorGradient(
555
+ crispIntensity,
556
+ backgroundColor,
557
+ edgeColor,
558
+ midColor,
559
+ coreColor,
560
+ )
561
+ const fluidColorChroma = vec3(
562
+ mix(fluidColor.x, fluidColor.x.mul(1.1), blurR.mul(0.3)),
563
+ fluidColor.y,
564
+ mix(fluidColor.z, fluidColor.z.mul(1.15), blurB.mul(0.3)),
565
+ )
566
+
567
+ const fluidMask = pow(clamp(blurIntensity, float(0), float(1)), float(1.2))
568
+ const crispMask = pow(clamp(crispIntensity, float(0), float(1)), float(0.96))
569
+ const fluidGlow = fluidColorChroma.mul(fluidMask).mul(1.8)
570
+ const crispGlow = crispColor.mul(crispMask).mul(1.95)
571
+ const crispWeight = crispMask.mul(this.crispBlendUniform)
572
+ let combined = mix(fluidGlow, crispGlow, crispWeight).add(fluidGlow.mul(0.15))
573
+
574
+ const grain = grainTexturePattern(
575
+ texUv.mul(vec2(this.grainScaleUniform, this.grainScaleUniform)),
576
+ )
577
+ .sub(0.5)
578
+ .mul(this.grainIntensityUniform)
579
+ combined = select(
580
+ this.grainEnabledUniform.greaterThan(float(0.5)),
581
+ combined.add(vec3(grain, grain, grain)),
582
+ combined,
583
+ )
584
+
585
+ const alpha = max(fluidMask, crispMask)
586
+ let finalColor = mix(backgroundColor, combined, smoothstep(float(0.01), float(0.85), alpha))
587
+ const vignetteUv = texUv.mul(2).sub(vec2(1, 1))
588
+ finalColor = finalColor.mul(float(1).sub(vignetteUv.dot(vignetteUv).mul(0.15)))
589
+
590
+ return vec4(clamp(finalColor, vec3(0, 0, 0), vec3(1, 1, 1)), float(1))
591
+ }
592
+
593
+ private applyColorGradient(
594
+ intensity: Node,
595
+ bgColor: Node,
596
+ edgeColor: Node,
597
+ midColor: Node,
598
+ coreColor: Node,
599
+ ): Node {
600
+ const t1 = smoothstep(float(0.6), float(0.95), intensity)
601
+ const t2 = smoothstep(float(0.2), float(0.65), intensity)
602
+ const t3 = smoothstep(float(0), float(0.25), intensity)
603
+ const edgeMixed = mix(bgColor, edgeColor, t3)
604
+ const midMixed = mix(edgeMixed, midColor, t2)
605
+ return mix(midMixed, coreColor, t1)
606
+ }
607
+
608
+ private ensureNoiseTexture(): void {
609
+ if (this.noiseTexture || this.noiseLoadStarted) {
610
+ return
611
+ }
612
+
613
+ this.noiseLoadStarted = true
614
+ void loadImageTexture("/textures/blue-noise.png")
615
+ .then((texture) => {
616
+ texture.wrapS = THREE.RepeatWrapping
617
+ texture.wrapT = THREE.RepeatWrapping
618
+ this.noiseTexture = texture
619
+ this.needsRefresh = true
620
+ })
621
+ .catch(() => {
622
+ this.needsRefresh = true
623
+ })
624
+ }
625
+
626
+ private trackBlurSampleNode(sampleUv: Node): Node {
627
+ const sampleNode = tslTexture(this.placeholder, sampleUv)
628
+ this.blurSampleNodes.push(sampleNode)
629
+ return sampleNode
630
+ }
631
+
632
+ private trackCompositeBlurNode(sampleUv: Node): Node {
633
+ const sampleNode = tslTexture(this.placeholder, sampleUv)
634
+ this.compositeBlurNodes.push(sampleNode)
635
+ return sampleNode
636
+ }
637
+
638
+ private trackNoiseSampleNode(sampleUv: Node): Node {
639
+ const sampleNode = tslTexture(this.placeholder, sampleUv)
640
+ this.noiseSampleNodes.push(sampleNode)
641
+ return sampleNode
642
+ }
643
+
644
+ private setColorUniform(target: Node, value: string): void {
645
+ const [r, g, b] = hexToRgb(value)
646
+ ;(target.value as THREE.Vector3).set(r, g, b)
647
+ }
648
+
649
+ private normalizeBloomRadius(value: number): number {
650
+ return clamp01(value / 24)
651
+ }
652
+
653
+ private normalizeBloomSoftness(value: number): number {
654
+ return Math.max(0.001, value * 0.25)
655
+ }
656
+
657
+ private disposeBloomNode(): void {
658
+ ;(this.bloomNode as { dispose?: () => void } | null)?.dispose?.()
659
+ }
660
+
661
+ private getBloomTextureNode(): Node {
662
+ const bloomNode = this.bloomNode as
663
+ | ({
664
+ getTexture?: () => Node
665
+ getTextureNode?: () => Node
666
+ } & object)
667
+ | null
668
+
669
+ if (!bloomNode) {
670
+ throw new Error("Bloom node is not initialized")
671
+ }
672
+
673
+ if ("getTextureNode" in bloomNode && typeof bloomNode.getTextureNode === "function") {
674
+ return bloomNode.getTextureNode()
675
+ }
676
+
677
+ if ("getTexture" in bloomNode && typeof bloomNode.getTexture === "function") {
678
+ return bloomNode.getTexture()
679
+ }
680
+
681
+ throw new Error("Bloom node does not expose a texture getter")
682
+ }
683
+ }