@call-me-sensei/toonlab 0.1.1

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 (146) hide show
  1. package/ATTRIBUTION.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/package.json +97 -0
  5. package/src/character/characterRig.js +353 -0
  6. package/src/character/freestyleSwimClip.js +541 -0
  7. package/src/character/index.js +4 -0
  8. package/src/character/modelLoader.js +433 -0
  9. package/src/core/materialRoles.js +438 -0
  10. package/src/core/presetDocuments.js +110 -0
  11. package/src/core/shaderBackend.js +24 -0
  12. package/src/debrisgen/debrisFields.js +544 -0
  13. package/src/debrisgen/debrisGenerator.js +1929 -0
  14. package/src/debrisgen/debrisPalettes.js +71 -0
  15. package/src/debrisgen/debrisPhysics.js +198 -0
  16. package/src/debrisgen/debrisPresets.js +155 -0
  17. package/src/debrisgen/debrisSettings.js +333 -0
  18. package/src/debrisgen/debrisTextures.js +380 -0
  19. package/src/debrisgen/index.js +5 -0
  20. package/src/debug/fieldValues.js +95 -0
  21. package/src/debug/index.js +7 -0
  22. package/src/debug/settingsPanel.js +170 -0
  23. package/src/environment/environmentAmbientProbe.js +160 -0
  24. package/src/environment/environmentMaterialAdapter.js +336 -0
  25. package/src/environment/environmentMaterialClassifier.js +178 -0
  26. package/src/environment/environmentPlanarReflection.js +201 -0
  27. package/src/environment/environmentPresets.js +443 -0
  28. package/src/environment/environmentRigs.js +684 -0
  29. package/src/environment/environmentSettings.js +504 -0
  30. package/src/environment/environmentShaderMaterials.js +318 -0
  31. package/src/environment/environmentSunShadowPass.js +268 -0
  32. package/src/environment/environmentTextureResolver.js +161 -0
  33. package/src/environment/environmentTimeOfDay.js +167 -0
  34. package/src/environment/environmentVertexAo.js +188 -0
  35. package/src/environment/index.js +8 -0
  36. package/src/environment/scanAssetStylize.js +103 -0
  37. package/src/index.js +21 -0
  38. package/src/loaders/index.js +1 -0
  39. package/src/post/index.js +2 -0
  40. package/src/post/postProcessing.js +1087 -0
  41. package/src/rockgen/export/glbExport.js +177 -0
  42. package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
  43. package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
  44. package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
  45. package/src/rockgen/index.js +14 -0
  46. package/src/rockgen/mesh/meshAttributes.js +374 -0
  47. package/src/rockgen/mesh/meshDocument.js +158 -0
  48. package/src/rockgen/mesh/surfaceNets.js +329 -0
  49. package/src/rockgen/noise/cellularNoise3.js +92 -0
  50. package/src/rockgen/noise/prng.js +62 -0
  51. package/src/rockgen/noise/simplexNoise3.js +89 -0
  52. package/src/rockgen/noise/valueNoise3.js +87 -0
  53. package/src/rockgen/rockDocument.js +255 -0
  54. package/src/rockgen/rockHelpers.js +8 -0
  55. package/src/rockgen/rockgenPresets.js +559 -0
  56. package/src/rockgen/rockgenSettings.js +756 -0
  57. package/src/rockgen/sdf/fieldCompiler.js +544 -0
  58. package/src/rockgen/sdf/sculptEdits.js +66 -0
  59. package/src/rockgen/sdf/sdfModifiers.js +45 -0
  60. package/src/rockgen/sdf/sdfOps.js +37 -0
  61. package/src/rockgen/sdf/sdfPrimitives.js +88 -0
  62. package/src/shaders-tsl/anime.js +963 -0
  63. package/src/shaders-tsl/chunks/character-color.js +64 -0
  64. package/src/shaders-tsl/chunks/character-highlights.js +174 -0
  65. package/src/shaders-tsl/chunks/character-lighting.js +309 -0
  66. package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
  67. package/src/shaders-tsl/chunks/character-roles.js +52 -0
  68. package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
  69. package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
  70. package/src/shaders-tsl/chunks/character-skinning.js +140 -0
  71. package/src/shaders-tsl/chunks/environment-color.js +47 -0
  72. package/src/shaders-tsl/chunks/environment-debug.js +79 -0
  73. package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
  74. package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
  75. package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
  76. package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
  77. package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
  78. package/src/shaders-tsl/chunks/water-color.js +190 -0
  79. package/src/shaders-tsl/chunks/water-common.js +124 -0
  80. package/src/shaders-tsl/chunks/water-foam.js +93 -0
  81. package/src/shaders-tsl/chunks/water-lighting.js +116 -0
  82. package/src/shaders-tsl/chunks/water-ripple.js +64 -0
  83. package/src/shaders-tsl/chunks/water-waves.js +100 -0
  84. package/src/shaders-tsl/environment-ao-overlay.js +60 -0
  85. package/src/shaders-tsl/environment.js +680 -0
  86. package/src/shaders-tsl/flower.js +267 -0
  87. package/src/shaders-tsl/grass.js +193 -0
  88. package/src/shaders-tsl/post-composite.js +461 -0
  89. package/src/shaders-tsl/sky.js +175 -0
  90. package/src/shaders-tsl/tree-leaf.js +264 -0
  91. package/src/shaders-tsl/water-breaker.js +423 -0
  92. package/src/shaders-tsl/water-kelp.js +171 -0
  93. package/src/shaders-tsl/water-rain.js +107 -0
  94. package/src/shaders-tsl/water-simulation.js +140 -0
  95. package/src/shaders-tsl/water-splash.js +298 -0
  96. package/src/shaders-tsl/water.js +509 -0
  97. package/src/sky/index.js +2 -0
  98. package/src/sky/stylizedSky.js +341 -0
  99. package/src/toon/characterRenderPasses.js +646 -0
  100. package/src/toon/index.js +4 -0
  101. package/src/toon/settings/alphaSettings.js +170 -0
  102. package/src/toon/settings/averageShadowSettings.js +181 -0
  103. package/src/toon/settings/baseTextureSettings.js +133 -0
  104. package/src/toon/settings/celShadeSettings.js +96 -0
  105. package/src/toon/settings/contactShadowSettings.js +114 -0
  106. package/src/toon/settings/eyeHighlightSettings.js +187 -0
  107. package/src/toon/settings/faceLightingSettings.js +135 -0
  108. package/src/toon/settings/furSettings.js +112 -0
  109. package/src/toon/settings/glitterSettings.js +102 -0
  110. package/src/toon/settings/hairHighlightSettings.js +325 -0
  111. package/src/toon/settings/indirectLightSettings.js +244 -0
  112. package/src/toon/settings/localLightSettings.js +176 -0
  113. package/src/toon/settings/materialMapSettings.js +299 -0
  114. package/src/toon/settings/outlineSettings.js +263 -0
  115. package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
  116. package/src/toon/settings/rimLightSettings.js +268 -0
  117. package/src/toon/settings/sceneShadowSettings.js +147 -0
  118. package/src/toon/settings/selfShadowSettings.js +221 -0
  119. package/src/toon/settings/shadowColorSettings.js +238 -0
  120. package/src/toon/settings/skinToneSettings.js +150 -0
  121. package/src/toon/settings/specularSettings.js +314 -0
  122. package/src/toon/settings/stickerSettings.js +102 -0
  123. package/src/toon/toonMaterialAdapter.js +2235 -0
  124. package/src/toon/toonSettings.js +860 -0
  125. package/src/vegetation/flowerSpecies.js +260 -0
  126. package/src/vegetation/index.js +10 -0
  127. package/src/vegetation/stylizedBush.js +136 -0
  128. package/src/vegetation/stylizedFlower.js +213 -0
  129. package/src/vegetation/stylizedFlowers.js +297 -0
  130. package/src/vegetation/stylizedGrass.js +491 -0
  131. package/src/vegetation/stylizedTree.js +2449 -0
  132. package/src/vegetation/stylizedTreeFoliage.js +691 -0
  133. package/src/vegetation/treeExport.js +300 -0
  134. package/src/vegetation/treeRecipe.js +647 -0
  135. package/src/water/index.js +2 -0
  136. package/src/water/water.js +20 -0
  137. package/src/water/waterBreakerSystem.js +591 -0
  138. package/src/water/waterInteraction.js +193 -0
  139. package/src/water/waterMaterial.js +263 -0
  140. package/src/water/waterRain.js +136 -0
  141. package/src/water/waterRippleSimulation.js +227 -0
  142. package/src/water/waterScenePasses.js +415 -0
  143. package/src/water/waterSettings.js +1138 -0
  144. package/src/water/waterSplashSystem.js +251 -0
  145. package/src/water/waterSurface.js +475 -0
  146. package/src/water/waterVegetation.js +83 -0
@@ -0,0 +1,461 @@
1
+ // TSL port of the post-processing final composite pass and the pyramid
2
+ // (dual-filter mip chain) bloom passes from src/post/postProcessing.js.
3
+ // Consumed by postProcessing.js's createFinalCompositeMaterial /
4
+ // createBloomPassMaterial call sites on the TSL backend; every material here
5
+ // exposes `.uniforms` under the exact GLSL uniform names so the shared,
6
+ // backend-agnostic settings code (applyCompositeSettings, renderBloomPyramid,
7
+ // setCharacterMask) keeps working unchanged on both backends
8
+ // (docs/tsl-conventions.md, "The .uniforms compatibility surface").
9
+ //
10
+ // ---------------------------------------------------------------------------
11
+ // Color-space audit
12
+ // ---------------------------------------------------------------------------
13
+ // This is the one place in the post stack where the node renderer's implicit
14
+ // output encode structurally differs from the classic pipeline's explicit
15
+ // encode, so read this before touching any texture read below.
16
+ //
17
+ // Classic: anime.frag.glsl / environment.frag.glsl / grass.frag.glsl each
18
+ // end with `#include <colorspace_fragment>` — but linearToOutputTexel is
19
+ // compiled PER RENDER TARGET: three picks the transfer function from the
20
+ // bound target's texture.colorSpace, not from renderer.outputColorSpace.
21
+ // The post pipeline's offscreen `target.texture` is explicitly NoColorSpace,
22
+ // so when classic renders the scene into it the baked include is an
23
+ // IDENTITY and the buffer holds LINEAR color; the one real sRGB encode
24
+ // happens in this composite's own trailing include when it draws to the
25
+ // sRGB canvas. Net classic behavior: encode(composite-math(linearScene)) —
26
+ // a SINGLE encode. (An earlier revision of this port assumed the include
27
+ // was unconditional and double-encoded; measured on the live pipeline,
28
+ // post-on classic differs from post-off classic only by the composite math
29
+ // itself, proving the single-encode reading.)
30
+ //
31
+ // Node renderer: color-space conversion is not baked into materials at all;
32
+ // it happens exactly once, automatically, as the renderer's terminal output
33
+ // pass when drawing to the default framebuffer (renderer/common/Renderer.js
34
+ // `_renderOutput`), and never when rendering into our own offscreen target.
35
+ // So the node chain ALREADY matches classic with no compensation: scene ->
36
+ // target holds linear color (same as classic's identity-encoded buffer),
37
+ // the composite math runs in linear (same domain as classic), and the
38
+ // terminal canvas pass supplies the single encode (same as classic's
39
+ // trailing include). Therefore: NO manual sRGBTransfer* call anywhere in
40
+ // this file — adding one washes the whole frame out by a spare encode
41
+ // (measured: +191/255 mean vs classic). renderer.toneMapping is
42
+ // NoToneMapping project-wide, so the tone-mapping half of the terminal pass
43
+ // is a no-op either way.
44
+ //
45
+ // tBloom / bloom-chain intermediate textures (downsample/upsample) are NOT
46
+ // re-encoded — they're already downstream of the prefilter's one manual
47
+ // encode, exactly like classic never re-encodes mid-chain either. tDepth is
48
+ // a linear window-depth value (never color-space encoded in either
49
+ // pipeline). tCharacterMask is a binary 0/1 mask (encode-invariant at both
50
+ // endpoints) so it is read raw.
51
+
52
+ import * as THREE from 'three';
53
+ import {
54
+ abs,
55
+ clamp,
56
+ distance,
57
+ dot,
58
+ float,
59
+ floor,
60
+ fract,
61
+ Fn,
62
+ If,
63
+ length,
64
+ max,
65
+ min,
66
+ mix,
67
+ positionLocal,
68
+ screenCoordinate,
69
+ sign,
70
+ smoothstep,
71
+ texture,
72
+ uniform,
73
+ uv,
74
+ vec2,
75
+ vec3,
76
+ vec4,
77
+ viewZToOrthographicDepth,
78
+ perspectiveDepthToViewZ,
79
+ } from 'three/tsl';
80
+ import { NodeMaterial } from 'three/webgpu';
81
+
82
+ // Shared 1x1 placeholder so every optional texture uniform (tLut,
83
+ // tCharacterMask, tBloom, and the not-yet-wired tDepth/tDiffuse at
84
+ // construction time) always has a real bound texture. Classic tolerates a
85
+ // `null` sampler uniform because it is only ever read inside an `if` guard
86
+ // that is false whenever the uniform is unset; the node backends bind every
87
+ // declared texture unconditionally regardless of runtime branching, so a
88
+ // `null` TextureNode.value is unsafe even inside a runtime-gated `If()`
89
+ // (docs/tsl-conventions.md gotcha 9 territory — same "always keep a valid
90
+ // fallback bound" rule as anime.js's fallbackWhiteTexture()).
91
+ let sharedFallbackTexture = null;
92
+ export function fallbackPostTexture() {
93
+ if (!sharedFallbackTexture) {
94
+ sharedFallbackTexture = new THREE.DataTexture(new Uint8Array([0, 0, 0, 255]), 1, 1);
95
+ sharedFallbackTexture.needsUpdate = true;
96
+ }
97
+ return sharedFallbackTexture;
98
+ }
99
+
100
+ function luma(color) {
101
+ return dot(color, vec3(0.2126, 0.7152, 0.0722));
102
+ }
103
+
104
+ function applySaturationHelper(color, amount) {
105
+ return mix(vec3(luma(color)), color, amount);
106
+ }
107
+
108
+ // Raw NDC passthrough matching the classic bloom passes' vertex shader
109
+ // (`gl_Position = vec4(position.xy, 0.0, 1.0)`), which ignores the camera
110
+ // entirely — the bloom quad mesh's own [-1,1] geometry IS clip space.
111
+ // Assigning `material.vertexNode` directly (rather than `positionNode`)
112
+ // bypasses NodeMaterial's default MVP transform, mirroring GLSL exactly.
113
+ const bloomQuadVertexNode = vec4(positionLocal.xy, 0.0, 1.0);
114
+
115
+ function createBloomPassMaterialBase(name) {
116
+ const material = new NodeMaterial();
117
+ material.name = name;
118
+ material.depthTest = false;
119
+ material.depthWrite = false;
120
+ material.fog = false;
121
+ material.lights = false;
122
+ material.vertexNode = bloomQuadVertexNode;
123
+ return material;
124
+ }
125
+
126
+ /**
127
+ * Bloom prefilter: thresholds target.texture (character-mask aware) into the
128
+ * first mip of the pyramid chain. TSL port of bloomPrefilterFragmentShader.
129
+ */
130
+ export function createBloomPrefilterNodeMaterial() {
131
+ const u = {
132
+ bloomBackgroundSuppress: uniform(1),
133
+ bloomCharacterBoost: uniform(1),
134
+ bloomThreshold: uniform(0.9),
135
+ tCharacterMask: texture(fallbackPostTexture()),
136
+ tInput: texture(fallbackPostTexture()),
137
+ useCharacterMask: uniform(0),
138
+ };
139
+
140
+ const material = createBloomPassMaterialBase('PostProcessing.BloomPrefilterNode');
141
+ material.fragmentNode = Fn(() => {
142
+ const sampleUv = uv();
143
+ // Raw linear read — see the file header color-space audit: the scene
144
+ // buffer is linear on both pipelines, exactly like classic's prefilter
145
+ // input; no transfer function anywhere in the chain.
146
+ const color = u.tInput.sample(sampleUv).level(0).rgb;
147
+ const brightness = luma(color);
148
+ const bright = smoothstep(u.bloomThreshold, 1.0, brightness);
149
+ const excess = max(color.sub(u.bloomThreshold), vec3(0.0));
150
+ const mask = u.tCharacterMask.sample(sampleUv).level(0).r;
151
+ // mix()-based equivalent of `if (useCharacterMask > 0.5) { ... }`: safe
152
+ // because useCharacterMask is always exactly 0.0 or 1.0 (never
153
+ // fractional), and tCharacterMask always has a valid fallback bound, so
154
+ // unconditionally sampling it changes nothing observable while avoiding
155
+ // an extra If() purely for a cheap scalar blend.
156
+ const maskWeight = mix(1.0, mix(u.bloomBackgroundSuppress, u.bloomCharacterBoost, mask), u.useCharacterMask);
157
+ return vec4(excess.mul(bright).mul(maskWeight), 1.0);
158
+ })();
159
+ material.uniforms = u;
160
+ return material;
161
+ }
162
+
163
+ /**
164
+ * Bloom downsample: 5-tap box filter, one pyramid level narrower. TSL port
165
+ * of bloomDownsampleFragmentShader.
166
+ */
167
+ export function createBloomDownsampleNodeMaterial() {
168
+ const u = {
169
+ tInput: texture(fallbackPostTexture()),
170
+ texelSize: uniform(new THREE.Vector2()),
171
+ };
172
+
173
+ const material = createBloomPassMaterialBase('PostProcessing.BloomDownsampleNode');
174
+ material.fragmentNode = Fn(() => {
175
+ const sampleUv = uv();
176
+ const halfTexel = u.texelSize.mul(0.5);
177
+ const sum = u.tInput.sample(sampleUv).level(0).rgb.mul(4.0).toVar();
178
+ sum.addAssign(u.tInput.sample(sampleUv.sub(halfTexel)).level(0).rgb);
179
+ sum.addAssign(u.tInput.sample(sampleUv.add(halfTexel)).level(0).rgb);
180
+ sum.addAssign(u.tInput.sample(sampleUv.add(vec2(halfTexel.x, halfTexel.y.negate()))).level(0).rgb);
181
+ sum.addAssign(u.tInput.sample(sampleUv.sub(vec2(halfTexel.x, halfTexel.y.negate()))).level(0).rgb);
182
+ return vec4(sum.div(8.0), 1.0);
183
+ })();
184
+ material.uniforms = u;
185
+ return material;
186
+ }
187
+
188
+ /**
189
+ * Bloom upsample: 8-tap tent filter, accumulating the narrower level back
190
+ * onto the wider one. TSL port of bloomUpsampleFragmentShader.
191
+ */
192
+ export function createBloomUpsampleNodeMaterial() {
193
+ const u = {
194
+ bloomSpread: uniform(1),
195
+ tAccumulate: texture(fallbackPostTexture()),
196
+ tInput: texture(fallbackPostTexture()),
197
+ texelSize: uniform(new THREE.Vector2()),
198
+ };
199
+
200
+ const material = createBloomPassMaterialBase('PostProcessing.BloomUpsampleNode');
201
+ material.fragmentNode = Fn(() => {
202
+ const sampleUv = uv();
203
+ const t = u.texelSize.mul(u.bloomSpread);
204
+ const sum = u.tInput.sample(sampleUv.add(vec2(t.x.mul(-2.0), 0.0))).level(0).rgb.toVar();
205
+ sum.addAssign(u.tInput.sample(sampleUv.add(vec2(t.x.negate(), t.y))).level(0).rgb.mul(2.0));
206
+ sum.addAssign(u.tInput.sample(sampleUv.add(vec2(0.0, t.y.mul(2.0)))).level(0).rgb);
207
+ sum.addAssign(u.tInput.sample(sampleUv.add(vec2(t.x, t.y))).level(0).rgb.mul(2.0));
208
+ sum.addAssign(u.tInput.sample(sampleUv.add(vec2(t.x.mul(2.0), 0.0))).level(0).rgb);
209
+ sum.addAssign(u.tInput.sample(sampleUv.add(vec2(t.x, t.y.negate()))).level(0).rgb.mul(2.0));
210
+ sum.addAssign(u.tInput.sample(sampleUv.add(vec2(0.0, t.y.mul(-2.0)))).level(0).rgb);
211
+ sum.addAssign(u.tInput.sample(sampleUv.add(vec2(t.x.negate(), t.y.negate()))).level(0).rgb.mul(2.0));
212
+ return vec4(sum.div(12.0).add(u.tAccumulate.sample(sampleUv).level(0).rgb), 1.0);
213
+ })();
214
+ material.uniforms = u;
215
+ return material;
216
+ }
217
+
218
+ /**
219
+ * Final composite: motion blur, single-pass or pyramid bloom add, depth cue,
220
+ * color grade, LUT, vertical grade, screen outline, vignette, strength
221
+ * blend, dither. TSL port of FinalCompositeShader. `.uniforms` exposes every
222
+ * GLSL uniform name so applyCompositeSettings() in postProcessing.js works
223
+ * unchanged.
224
+ */
225
+ export function createPostCompositeNodeMaterial() {
226
+ const u = {
227
+ bloomBackgroundSuppress: uniform(1.0),
228
+ bloomCharacterBoost: uniform(1.0),
229
+ bloomRadius: uniform(0.28),
230
+ bloomStrength: uniform(0.0),
231
+ bloomThreshold: uniform(0.84),
232
+ tBloom: texture(fallbackPostTexture()),
233
+ useBloomTexture: uniform(0.0),
234
+ tCharacterMask: texture(fallbackPostTexture()),
235
+ useCharacterMask: uniform(0.0),
236
+ useMotionBlur: uniform(0.0),
237
+ motionBlurStrength: uniform(0.0),
238
+ motionBlurInverseViewProjection: uniform(new THREE.Matrix4()),
239
+ motionBlurPreviousViewProjection: uniform(new THREE.Matrix4()),
240
+ bottomDark: uniform(0.0),
241
+ cameraFar: uniform(100.0),
242
+ cameraNear: uniform(0.1),
243
+ contrast: uniform(1.0),
244
+ depthCueColor: uniform(new THREE.Color(0x9db7d8)),
245
+ depthCueFar: uniform(24.0),
246
+ depthCueNear: uniform(1.0),
247
+ depthCueStrength: uniform(0.0),
248
+ exposure: uniform(1.0),
249
+ tLut: texture(fallbackPostTexture()),
250
+ lutSize: uniform(0.0),
251
+ lutStrength: uniform(0.0),
252
+ outlineColor: uniform(new THREE.Color(0x10131a)),
253
+ outlineDepthStrength: uniform(0.0),
254
+ outlineLumaStrength: uniform(0.0),
255
+ outlineStrength: uniform(0.0),
256
+ resolution: uniform(new THREE.Vector2(1, 1)),
257
+ saturation: uniform(1.0),
258
+ strength: uniform(1.0),
259
+ // Float-color scene depth (docs/tsl-conventions.md gotcha 5) written by
260
+ // the auto-gated prepass in postProcessing.js — NOT a classic
261
+ // THREE.DepthTexture. Same [0,1] window-depth convention, so
262
+ // perspectiveDepthToViewZ below is unchanged from the classic formula.
263
+ tDepth: texture(fallbackPostTexture()),
264
+ tDiffuse: texture(fallbackPostTexture()),
265
+ topLight: uniform(0.0),
266
+ useColorGrade: uniform(0.0),
267
+ useDepthCue: uniform(0.0),
268
+ useScreenOutline: uniform(0.0),
269
+ useVignette: uniform(0.0),
270
+ useVerticalGrade: uniform(0.0),
271
+ vignetteRadius: uniform(0.72),
272
+ vignetteSoftness: uniform(0.34),
273
+ vignetteStrength: uniform(0.0),
274
+ warmth: uniform(0.0),
275
+ };
276
+
277
+ // ---- Helpers (closures over u; docs/tsl-conventions.md "leaf helpers
278
+ // that map nodes->nodes are Fn() exports") ----
279
+
280
+ // RT-orientation gap (docs/tsl-conventions.md gotcha 6, projective-sample
281
+ // form): this material is the one RT->canvas pass in the chain, and the
282
+ // node backends store render-target rows top-down relative to the canvas
283
+ // quad's uv — every fetch from an offscreen target must flip v. Screen-
284
+ // anchored math (vignette, bottom/top gradients, NDC reconstruction) keeps
285
+ // the raw quad uv; RT->RT bloom passes are self-consistent and don't flip.
286
+ const rtUv = Fn(([sampleUv]) => vec2(sampleUv.x, sampleUv.y.oneMinus()));
287
+
288
+ // Raw linear read — see the file header color-space audit: the offscreen
289
+ // buffer holds linear color on BOTH pipelines (classic's baked include is
290
+ // identity for a NoColorSpace target), so no transfer function here.
291
+ const sampleSceneColor = Fn(([sampleUv]) => {
292
+ return u.tDiffuse.sample(rtUv(sampleUv)).level(0).rgb;
293
+ });
294
+
295
+ const readLinearDepth = Fn(([sampleUv]) => {
296
+ const fragCoordZ = u.tDepth.sample(rtUv(sampleUv)).level(0).x;
297
+ const viewZ = perspectiveDepthToViewZ(fragCoordZ, u.cameraNear, u.cameraFar);
298
+ return viewZToOrthographicDepth(viewZ, u.cameraNear, u.cameraFar);
299
+ });
300
+
301
+ const sampleLutSlice = Fn(([color, slice]) => {
302
+ const uCoord = slice.mul(u.lutSize).add(0.5).add(color.r.mul(u.lutSize.sub(1.0))).div(u.lutSize.mul(u.lutSize));
303
+ const vCoord = float(0.5).add(color.g.mul(u.lutSize.sub(1.0))).div(u.lutSize);
304
+ return u.tLut.sample(vec2(uCoord, vCoord)).level(0).rgb;
305
+ });
306
+
307
+ const applyLut = Fn(([color]) => {
308
+ const clamped = clamp(color, 0.0, 1.0).toVar();
309
+ const blue = clamped.b.mul(u.lutSize.sub(1.0));
310
+ const slice0 = floor(blue);
311
+ const slice1 = min(slice0.add(1.0), u.lutSize.sub(1.0));
312
+ return mix(sampleLutSlice(clamped, slice0), sampleLutSlice(clamped, slice1), blue.sub(slice0));
313
+ });
314
+
315
+ const brightSample = Fn(([sampleUv]) => {
316
+ const color = sampleSceneColor(sampleUv);
317
+ const bright = smoothstep(u.bloomThreshold, 1.0, luma(color));
318
+ const excess = max(color.sub(u.bloomThreshold), vec3(0.0));
319
+ const mask = u.tCharacterMask.sample(rtUv(sampleUv)).level(0).r;
320
+ const maskWeight = mix(1.0, mix(u.bloomBackgroundSuppress, u.bloomCharacterBoost, mask), u.useCharacterMask);
321
+ return excess.mul(bright).mul(maskWeight);
322
+ });
323
+
324
+ const bloomSample = Fn(([texel, centerUv]) => {
325
+ const radius = max(u.bloomRadius, 0.001).mul(8.0);
326
+ const offsetX = vec2(texel.x.mul(radius), 0.0);
327
+ const offsetY = vec2(0.0, texel.y.mul(radius));
328
+ const offsetD = vec2(texel.x.mul(radius).mul(0.7071), texel.y.mul(radius).mul(0.7071));
329
+
330
+ const bloom = brightSample(centerUv).mul(0.2).toVar();
331
+ bloom.addAssign(brightSample(centerUv.add(offsetX)).mul(0.1));
332
+ bloom.addAssign(brightSample(centerUv.sub(offsetX)).mul(0.1));
333
+ bloom.addAssign(brightSample(centerUv.add(offsetY)).mul(0.1));
334
+ bloom.addAssign(brightSample(centerUv.sub(offsetY)).mul(0.1));
335
+ bloom.addAssign(brightSample(centerUv.add(offsetD)).mul(0.075));
336
+ bloom.addAssign(brightSample(centerUv.sub(offsetD)).mul(0.075));
337
+ bloom.addAssign(brightSample(centerUv.add(vec2(offsetD.x, offsetD.y.negate()))).mul(0.075));
338
+ bloom.addAssign(brightSample(centerUv.add(vec2(offsetD.x.negate(), offsetD.y))).mul(0.075));
339
+ return bloom;
340
+ });
341
+
342
+ const lumaEdge = Fn(([texel, centerUv]) => {
343
+ const lumaAt = (offsetUv) => luma(sampleSceneColor(offsetUv));
344
+ const center = lumaAt(centerUv);
345
+ const horizontal = abs(lumaAt(centerUv.add(vec2(texel.x, 0.0))).sub(center))
346
+ .add(abs(lumaAt(centerUv.sub(vec2(texel.x, 0.0))).sub(center)));
347
+ const vertical = abs(lumaAt(centerUv.add(vec2(0.0, texel.y))).sub(center))
348
+ .add(abs(lumaAt(centerUv.sub(vec2(0.0, texel.y))).sub(center)));
349
+ return horizontal.add(vertical);
350
+ });
351
+
352
+ const depthEdge = Fn(([texel, centerUv]) => {
353
+ const center = readLinearDepth(centerUv);
354
+ const horizontal = abs(readLinearDepth(centerUv.add(vec2(texel.x, 0.0))).sub(center))
355
+ .add(abs(readLinearDepth(centerUv.sub(vec2(texel.x, 0.0))).sub(center)));
356
+ const vertical = abs(readLinearDepth(centerUv.add(vec2(0.0, texel.y))).sub(center))
357
+ .add(abs(readLinearDepth(centerUv.sub(vec2(0.0, texel.y))).sub(center)));
358
+ return horizontal.add(vertical);
359
+ });
360
+
361
+ const material = new NodeMaterial();
362
+ material.name = 'PostProcessing.FinalCompositeNode';
363
+ material.depthTest = false;
364
+ material.depthWrite = false;
365
+ material.fog = false;
366
+ material.lights = false;
367
+
368
+ material.fragmentNode = Fn(() => {
369
+ const vUv = uv();
370
+
371
+ const rawSource = u.tDiffuse.sample(rtUv(vUv)).level(0).toVar();
372
+ const sourceAlpha = rawSource.a;
373
+ const originalColor = rawSource.rgb.toVar();
374
+
375
+ If(u.useMotionBlur.greaterThan(0.5), () => {
376
+ const fragDepth = u.tDepth.sample(rtUv(vUv)).level(0).x;
377
+ const ndc = vec4(vUv.mul(2.0).sub(1.0), fragDepth.mul(2.0).sub(1.0), 1.0);
378
+ const worldPos = u.motionBlurInverseViewProjection.mul(ndc).toVar();
379
+ worldPos.assign(worldPos.div(worldPos.w));
380
+ const prevClip = u.motionBlurPreviousViewProjection.mul(worldPos);
381
+ const prevUv = prevClip.xy.div(max(abs(prevClip.w), 1e-6)).mul(sign(prevClip.w)).mul(0.5).add(0.5);
382
+ const velocity = vUv.sub(prevUv).mul(u.motionBlurStrength).toVar();
383
+ const speed = length(velocity);
384
+ If(speed.greaterThan(0.0001), () => {
385
+ velocity.assign(velocity.mul(min(1.0, float(0.05).div(speed))));
386
+ const blurSum = originalColor.toVar();
387
+ for (let i = 1; i < 8; i += 1) {
388
+ blurSum.addAssign(sampleSceneColor(vUv.add(velocity.mul(float(i).div(7.0).sub(0.5)))));
389
+ }
390
+ originalColor.assign(blurSum.div(8.0));
391
+ });
392
+ });
393
+
394
+ const color = originalColor.toVar();
395
+ const texel = vec2(1.0).div(max(u.resolution, vec2(1.0)));
396
+
397
+ If(u.useBloomTexture.greaterThan(0.5), () => {
398
+ color.addAssign(u.tBloom.sample(rtUv(vUv)).level(0).rgb.mul(u.bloomStrength));
399
+ }).ElseIf(u.bloomStrength.greaterThan(0.0), () => {
400
+ color.addAssign(bloomSample(texel, vUv).mul(u.bloomStrength));
401
+ });
402
+
403
+ If(u.useDepthCue.greaterThan(0.5), () => {
404
+ const depth = readLinearDepth(vUv);
405
+ const cue = smoothstep(u.depthCueNear, u.depthCueFar, depth).mul(u.depthCueStrength);
406
+ color.assign(mix(color, u.depthCueColor, cue));
407
+ });
408
+
409
+ If(u.useColorGrade.greaterThan(0.5), () => {
410
+ color.mulAssign(u.exposure);
411
+ color.assign(color.sub(0.5).mul(u.contrast).add(0.5));
412
+ color.assign(applySaturationHelper(color, u.saturation));
413
+ color.mulAssign(vec3(
414
+ u.warmth.add(1.0),
415
+ u.warmth.mul(0.35).add(1.0),
416
+ u.warmth.mul(-0.55).add(1.0),
417
+ ));
418
+ });
419
+
420
+ If(u.lutStrength.greaterThan(0.0).and(u.lutSize.greaterThan(1.5)), () => {
421
+ color.assign(mix(color, applyLut(color), clamp(u.lutStrength, 0.0, 1.0)));
422
+ });
423
+
424
+ If(u.useVerticalGrade.greaterThan(0.5), () => {
425
+ const top = smoothstep(0.36, 1.0, vUv.y).mul(u.topLight);
426
+ const bottom = smoothstep(0.0, 0.72, vUv.y).oneMinus().mul(u.bottomDark);
427
+ color.addAssign(vec3(1.0, 0.92, 0.78).mul(top));
428
+ color.mulAssign(bottom.oneMinus());
429
+ });
430
+
431
+ If(u.useScreenOutline.greaterThan(0.5), () => {
432
+ const edge = lumaEdge(texel, vUv).mul(u.outlineLumaStrength).add(depthEdge(texel, vUv).mul(u.outlineDepthStrength));
433
+ const edgeMask = smoothstep(0.02, 0.18, edge).mul(u.outlineStrength);
434
+ color.assign(mix(color, u.outlineColor, clamp(edgeMask, 0.0, 1.0)));
435
+ });
436
+
437
+ If(u.useVignette.greaterThan(0.5), () => {
438
+ const dist = distance(vUv, vec2(0.5));
439
+ const vignetteMask = smoothstep(u.vignetteRadius, u.vignetteRadius.add(u.vignetteSoftness), dist);
440
+ color.mulAssign(vignetteMask.mul(u.vignetteStrength).oneMinus());
441
+ });
442
+
443
+ color.assign(mix(originalColor, color, clamp(u.strength, 0.0, 1.0)));
444
+
445
+ // Interleaved-gradient dither (see FinalCompositeShader) — pixel
446
+ // coordinates via screenCoordinate, the TSL equivalent of gl_FragCoord.xy
447
+ // already used the same way in anime.js's Bayer dither.
448
+ const gradientNoise = fract(dot(screenCoordinate.xy, vec2(0.06711056, 0.00583715)));
449
+ const dither = fract(gradientNoise.mul(52.9829189));
450
+ color.addAssign(dither.sub(0.5).mul(1.5 / 255.0));
451
+
452
+ // No manual tone-mapping/color-space re-encode here — see the file
453
+ // header color-space audit. The renderer's automatic terminal-canvas
454
+ // output pass supplies the second encode (matching classic's own second
455
+ // `colorspace_fragment` include); adding one here would double-apply it.
456
+ return vec4(max(color, vec3(0.0)), sourceAlpha);
457
+ })();
458
+
459
+ material.uniforms = u;
460
+ return material;
461
+ }
@@ -0,0 +1,175 @@
1
+ // TSL port of src/shaders/sky.vert.glsl + sky.frag.glsl — stylized procedural
2
+ // sky dome. Vertical gradient, sun disc with glow, painterly two-tone fbm
3
+ // clouds, and twinkling stars. Fully procedural; pairs with the water's
4
+ // procedural sky fallback.
5
+ //
6
+ // Parity notes vs the GLSL ShaderMaterial:
7
+ // - The dome renders at the far plane via a custom vertexNode
8
+ // (gl_Position.z = w * 0.99995 in the GLSL vertex stage).
9
+ // - GLSL ends with tonemapping_fragment + colorspace_fragment; the node
10
+ // renderer applies the same output transforms automatically.
11
+ // - The material exposes `.uniforms` (node objects keyed by the GLSL uniform
12
+ // names, each with a `.value`) so applySkySettingsToMaterial and
13
+ // StylizedSky.update write the same slots on both backends.
14
+
15
+ import * as THREE from 'three';
16
+ import {
17
+ acos,
18
+ cameraProjectionMatrix,
19
+ cameraViewMatrix,
20
+ clamp,
21
+ dot,
22
+ float,
23
+ Fn,
24
+ If,
25
+ max,
26
+ mix,
27
+ modelWorldMatrix,
28
+ normalize,
29
+ positionLocal,
30
+ pow,
31
+ select,
32
+ smoothstep,
33
+ step,
34
+ uniform,
35
+ vec2,
36
+ vec3,
37
+ vec4,
38
+ } from 'three/tsl';
39
+ import { MeshBasicNodeMaterial } from 'three/webgpu';
40
+
41
+ import { waterFbm, waterHash12, waterToonStep, waterVoronoi2 } from './chunks/water-common.js';
42
+
43
+ export function createSkyNodeMaterial() {
44
+ const uniforms = {
45
+ uCloudColor: uniform(new THREE.Color()),
46
+ uCloudCoverage: uniform(0),
47
+ uCloudScale: uniform(0),
48
+ uCloudShadeColor: uniform(new THREE.Color()),
49
+ uCloudSpeed: uniform(0),
50
+ uGroundColor: uniform(new THREE.Color()),
51
+ uHorizonColor: uniform(new THREE.Color()),
52
+ uHorizonScattering: uniform(0),
53
+ uStarsStrength: uniform(0),
54
+ uSunColor: uniform(new THREE.Color()),
55
+ uSunDirection: uniform(new THREE.Vector3(0, 1, 0)),
56
+ uSunGlowStrength: uniform(0),
57
+ uSunSize: uniform(0),
58
+ uTime: uniform(0),
59
+ uZenithColor: uniform(new THREE.Color()),
60
+ };
61
+
62
+ const material = new MeshBasicNodeMaterial({
63
+ depthWrite: false,
64
+ side: THREE.BackSide,
65
+ });
66
+ material.name = 'StylizedSky';
67
+ material.fog = false;
68
+ material.lights = false;
69
+
70
+ // Vertex stage: dome pinned to the far plane regardless of sphere radius,
71
+ // so it never clips scene geometry and works inside reflection passes.
72
+ const clipPosition = cameraProjectionMatrix
73
+ .mul(cameraViewMatrix)
74
+ .mul(modelWorldMatrix)
75
+ .mul(vec4(positionLocal, 1.0));
76
+ material.vertexNode = vec4(
77
+ clipPosition.x,
78
+ clipPosition.y,
79
+ clipPosition.w.mul(0.99995),
80
+ clipPosition.w,
81
+ );
82
+
83
+ material.colorNode = Fn(() => {
84
+ const direction = normalize(positionLocal).toVar();
85
+ const up = direction.y.toVar();
86
+
87
+ // Base vertical gradient with a mirrored ground fade below the horizon.
88
+ const zenithMix = pow(clamp(up, 0.0, 1.0), 0.48);
89
+ const color = select(
90
+ up.greaterThanEqual(0.0),
91
+ mix(uniforms.uHorizonColor, uniforms.uZenithColor, zenithMix),
92
+ mix(uniforms.uHorizonColor, uniforms.uGroundColor, pow(clamp(up.negate(), 0.0, 1.0), 0.55)),
93
+ ).toVar();
94
+
95
+ // Sun disc and glow.
96
+ const cosAngle = dot(direction, uniforms.uSunDirection).toVar();
97
+ const angle = acos(clamp(cosAngle, -1.0, 1.0));
98
+ const sunDisc = smoothstep(uniforms.uSunSize.mul(0.5), uniforms.uSunSize, angle)
99
+ .oneMinus()
100
+ .mul(smoothstep(0.55, 0.82, uniforms.uCloudCoverage).oneMinus());
101
+ const sunGlow = pow(max(cosAngle, 0.0), 5.0).mul(0.16)
102
+ .add(pow(max(cosAngle, 0.0), 60.0).mul(0.5))
103
+ .toVar();
104
+ color.addAssign(
105
+ uniforms.uSunColor.mul(sunDisc.mul(2.4).add(sunGlow.mul(uniforms.uSunGlowStrength))),
106
+ );
107
+
108
+ // Atmospheric scattering hint: a warm wedge along the sun-side horizon
109
+ // that fades with altitude.
110
+ const horizonBand = smoothstep(0.0, 0.42, up.abs()).oneMinus();
111
+ const sunward = pow(clamp(cosAngle.mul(0.5).add(0.5), 0.0, 1.0), 5.0);
112
+ color.addAssign(
113
+ uniforms.uSunColor
114
+ .mul(horizonBand)
115
+ .mul(sunward)
116
+ .mul(uniforms.uHorizonScattering)
117
+ .mul(0.35),
118
+ );
119
+
120
+ // Stars: sparse grid glints, hidden below the horizon and near the sun.
121
+ // The If guard matches the GLSL branch — it also keeps the projected UV
122
+ // division (up + 0.28 crosses zero below the horizon) out of reach.
123
+ If(uniforms.uStarsStrength.greaterThan(0.001).and(up.greaterThan(0.02)), () => {
124
+ const starUv = direction.xz.div(up.add(0.28)).mul(14.0);
125
+ const starVoro = waterVoronoi2(starUv);
126
+ const starRandom = waterHash12(starVoro.zw);
127
+ const twinkle = uniforms.uTime
128
+ .mul(starRandom.mul(2.4).add(1.2))
129
+ .add(starRandom.mul(31.0))
130
+ .sin()
131
+ .mul(0.4)
132
+ .add(0.6);
133
+ const star = smoothstep(0.0, 0.06, starVoro.x).oneMinus().mul(step(0.72, starRandom));
134
+ color.addAssign(
135
+ vec3(1.0, 0.98, 0.92)
136
+ .mul(star)
137
+ .mul(twinkle)
138
+ .mul(uniforms.uStarsStrength)
139
+ .mul(smoothstep(0.03, 0.24, up))
140
+ .mul(sunGlow.oneMinus()),
141
+ );
142
+ });
143
+
144
+ // Painterly clouds: fbm silhouettes with a toon-stepped shadow side and a
145
+ // silver lining toward the sun, projected onto a virtual cloud plane.
146
+ If(uniforms.uCloudCoverage.greaterThan(0.001).and(up.greaterThan(0.015)), () => {
147
+ const cloudUv = direction.xz
148
+ .div(up.add(0.22))
149
+ .mul(uniforms.uCloudScale)
150
+ .add(uniforms.uTime.mul(uniforms.uCloudSpeed).mul(vec2(0.021, 0.006)))
151
+ .toVar();
152
+ const cloudBase = waterFbm(cloudUv, 4).toVar();
153
+ const threshold = mix(0.74, 0.34, clamp(uniforms.uCloudCoverage, 0.0, 1.0));
154
+ const cloudCore = smoothstep(threshold.add(0.1), threshold.add(0.24), cloudBase);
155
+ const cloudEdge = smoothstep(threshold, threshold.add(0.1), cloudBase);
156
+ const cloudMask = max(cloudCore, cloudEdge.mul(0.65));
157
+
158
+ const litSample = waterFbm(cloudUv.sub(uniforms.uSunDirection.xz.mul(0.4)), 4);
159
+ const shade = waterToonStep(float(0.02), float(0.06), cloudBase.sub(litSample));
160
+ const cloudColor = mix(uniforms.uCloudColor, uniforms.uCloudShadeColor, shade.mul(0.85))
161
+ .add(uniforms.uSunColor.mul(pow(max(cosAngle, 0.0), 10.0)).mul(0.3))
162
+ .toVar();
163
+
164
+ const horizonFade = smoothstep(0.015, 0.16, up);
165
+ color.assign(mix(color, cloudColor, cloudMask.mul(horizonFade)));
166
+ });
167
+
168
+ return vec4(color, 1.0);
169
+ })();
170
+
171
+ // Same-name uniform slots as the ShaderMaterial (UniformNodes expose
172
+ // `.value` just like ShaderMaterial uniform entries).
173
+ material.uniforms = uniforms;
174
+ return material;
175
+ }