@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,680 @@
1
+ // TSL port of src/shaders/environment.vert.glsl + environment.frag.glsl — the
2
+ // stitched environment surface shader. Consumed by environmentShaderMaterials'
3
+ // createEnvironmentMaterial on the TSL backend; receives the same resolved
4
+ // role/texture decisions the GLSL ShaderMaterial uniform block is built from,
5
+ // and exposes `.uniforms` under the exact GLSL uniform names so every
6
+ // adapter/HUD/settings write-through works unchanged on both backends.
7
+ //
8
+ // Porting notes (docs/tsl-conventions.md):
9
+ // - Optional samplers are gated by the same conditions as the GLSL defines
10
+ // (JS guards instead of #ifdef), so unused maps never join the graph and
11
+ // ANGLE's 16-sampler budget is respected on the fallback backend. The
12
+ // texture *uniform slots* still exist for every map (fallback-backed), so
13
+ // `.value` write-throughs keep working exactly like the classic uniforms.
14
+ // - GLSL float-uniform "bools" (enableX > 0.5) stay float uniforms with the
15
+ // same comparisons.
16
+ // - vWorldNormal is ported faithfully as three's VIEW-space normal
17
+ // (normalize(transformedNormal) in the GLSL vertex stage) — the mixed-space
18
+ // lighting quirk is part of the approved baseline look.
19
+ // - Scene lights come from chunks/character-scene-lights.js (shared uniforms
20
+ // mirrored from the scene each frame by the converted meshes'
21
+ // onBeforeRender) instead of <lights_pars_begin>.
22
+ // - getShadowMask() maps to sampleEnvironmentSunShadow(vWorldPosition)
23
+ // (chunks/environment-sun-shadow.js) — inert (mask = 1) until the dedicated
24
+ // sun-shadow pass fills the shared uniforms.
25
+ // - Debug views are always compiled in (they add zero bindings); the GLSL
26
+ // ENV_DEBUG_VIEWS define flip becomes a pure envDebugMode uniform write.
27
+ // - Scene-wide state (clock, cloud shadows, openings, probe, debug mode,
28
+ // planar reflection pass output) lives in module-level shared uniform nodes
29
+ // mirroring environmentShaderMaterials' environmentSharedUniforms.
30
+
31
+ import * as THREE from 'three';
32
+ import {
33
+ attribute,
34
+ cameraFar,
35
+ cameraNear,
36
+ cameraPosition,
37
+ cameraProjectionMatrix,
38
+ cameraViewMatrix,
39
+ clamp,
40
+ Discard,
41
+ distance,
42
+ dot,
43
+ exp,
44
+ float,
45
+ Fn,
46
+ frontFacing,
47
+ If,
48
+ length,
49
+ max,
50
+ mix,
51
+ normalize,
52
+ normalLocal,
53
+ positionWorld,
54
+ pow,
55
+ select,
56
+ smoothstep,
57
+ texture,
58
+ transformNormalToView,
59
+ uniform,
60
+ uniformArray,
61
+ uv,
62
+ varying,
63
+ vec2,
64
+ vec3,
65
+ vec4,
66
+ vertexColor,
67
+ viewZToOrthographicDepth,
68
+ viewZToPerspectiveDepth,
69
+ } from 'three/tsl';
70
+ import { NodeMaterial } from 'three/webgpu';
71
+
72
+ import {
73
+ fallbackEnvironmentBlackTexture,
74
+ fallbackEnvironmentNormalTexture,
75
+ fallbackEnvironmentWhiteTexture,
76
+ } from '../environment/environmentTextureResolver.js';
77
+ import { applySaturation, envLuma, windowPaneMask } from './chunks/environment-color.js';
78
+ import { environmentDebugColor } from './chunks/environment-debug.js';
79
+ import { createEnvironmentLightingChunk } from './chunks/environment-lighting.js';
80
+ import { sampleEnvironmentSunShadow } from './chunks/environment-sun-shadow.js';
81
+ import { stylizedCloudShadow } from './chunks/stylized-cloud-shadow.js';
82
+ import { toonSceneLights } from './chunks/character-scene-lights.js';
83
+
84
+ // Scene-wide uniform nodes shared by reference across every environment node
85
+ // material — the TSL mirror of environmentSharedUniforms (same names, same
86
+ // defaults). environmentShaderMaterials' setters write both stores; the
87
+ // per-material `.uniforms` maps register these shared nodes so material-walk
88
+ // writes (applyEnvironmentSettingsToMaterial cloudShadow* etc.) hit the
89
+ // scene-wide values exactly like the classic shared-by-reference uniforms.
90
+ export const environmentSharedUniformNodes = {
91
+ time: uniform(0.0),
92
+ cloudShadowStrength: uniform(0.0),
93
+ cloudShadowCoverage: uniform(0.45),
94
+ cloudShadowScale: uniform(0.012),
95
+ cloudShadowVelocity: uniform(new THREE.Vector2(0.02, 0.006)),
96
+ envDebugMode: uniform(0.0),
97
+ environmentOpenings: uniformArray(
98
+ Array.from({ length: 4 }, () => new THREE.Vector4()),
99
+ 'vec4',
100
+ ),
101
+ environmentOpeningCount: uniform(0.0),
102
+ ambientProbe: uniformArray(
103
+ Array.from({ length: 6 }, () => new THREE.Color(1, 1, 1)),
104
+ 'color',
105
+ ),
106
+ planarReflectionMap: texture(fallbackEnvironmentBlackTexture),
107
+ planarReflectionMatrix: uniform(new THREE.Matrix4()),
108
+ };
109
+
110
+ function max3(color) {
111
+ return max(color.r, max(color.g, color.b));
112
+ }
113
+
114
+ /**
115
+ * Builds the environment NodeMaterial. Value defaults mirror the classic
116
+ * ShaderMaterial uniform block in environmentShaderMaterials.js line for
117
+ * line; `flags` mirrors the USE_ENV_* defines the classic factory sets.
118
+ */
119
+ export function createEnvironmentNodeMaterial({
120
+ alphaBlend = false,
121
+ alphaCutoff = -1.0,
122
+ baseColor = new THREE.Color(1, 1, 1),
123
+ environmentBox = null,
124
+ flags,
125
+ hasSun = false,
126
+ isEmissive = false,
127
+ isFoliage = false,
128
+ isGlossFloor = false,
129
+ opacity = 1,
130
+ side = THREE.DoubleSide,
131
+ textureSet,
132
+ }) {
133
+ const shared = environmentSharedUniformNodes;
134
+
135
+ // ---- Uniforms (GLSL names; UniformNodes expose `.value` like
136
+ // ShaderMaterial uniform entries). Values mirror the classic block. ----
137
+ const u = {
138
+ baseMap: texture(textureSet.baseMap ?? fallbackEnvironmentWhiteTexture),
139
+ // Applies the base map's repeat/offset/rotation (THREE.Texture.matrix)
140
+ // like the built-in materials' mapTransform; identity when unset.
141
+ baseMapTransform: uniform(new THREE.Matrix3()),
142
+ alphaMap: texture(textureSet.alphaMap ?? fallbackEnvironmentWhiteTexture),
143
+ packedMap: texture(textureSet.packedMap ?? fallbackEnvironmentBlackTexture),
144
+ baseColor: uniform(baseColor.clone?.() ?? new THREE.Color(baseColor)),
145
+ opacity: uniform(opacity),
146
+ alphaCutoff: uniform(alphaCutoff),
147
+ useAlphaMap: uniform(textureSet.alphaMap ? 1.0 : 0.0),
148
+ alphaFromLuminance: uniform(textureSet.alphaFromLuminance ? 1.0 : 0.0),
149
+ foliageCutout: uniform(isFoliage ? 1.0 : 0.0),
150
+ usePackedMap: uniform(textureSet.packedMap ? 1.0 : 0.0),
151
+ windowCutout: uniform(0.0),
152
+ shadeStrength: uniform(hasSun ? 0.95 : 0.42),
153
+ shadeSoftness: uniform(hasSun ? 0.18 : 0.38),
154
+ ambientStrength: uniform(hasSun ? 0.0 : 0.98),
155
+ ambientLightInfluence: uniform(0.22),
156
+ directLightStrength: uniform(hasSun ? 1.35 : 0.42),
157
+ pointLightStrength: uniform(hasSun ? 0.72 : 0.18),
158
+ spotLightStrength: uniform(0.0),
159
+ exposure: uniform(hasSun ? 0.9 : 1.0),
160
+ lightingInfluence: uniform(hasSun ? 1.0 : 0.36),
161
+ saturation: uniform(1.12),
162
+ shadowLift: uniform(hasSun ? 0.0 : 0.66),
163
+ skyTintStrength: uniform(0.24),
164
+ emissiveStrength: uniform(hasSun ? 0.0 : isEmissive ? 0.55 : 0.0),
165
+ sunBoost: uniform(hasSun ? 0.08 : 0.0),
166
+ environmentCenter: uniform(environmentBox?.getCenter(new THREE.Vector3()) ?? new THREE.Vector3()),
167
+ environmentSize: uniform(environmentBox?.getSize(new THREE.Vector3()) ?? new THREE.Vector3(1, 1, 1)),
168
+ leftSideShadow: uniform(hasSun ? 0.92 : 0.0),
169
+ leftSideShadowColor: uniform(new THREE.Color(0.34, 0.29, 0.25)),
170
+ packedOcclusionStrength: uniform(0.22),
171
+ bakedGlowStrength: uniform(0.05),
172
+ shadowTintColor: uniform(new THREE.Color(0.86, 0.82, 0.78)),
173
+ skyGroundTint: uniform(new THREE.Color(1.05, 0.97, 0.9)),
174
+ skyTopTint: uniform(new THREE.Color(0.86, 0.96, 1.08)),
175
+ sunBoostColor: uniform(new THREE.Color(1.0, 0.78, 0.42)),
176
+
177
+ normalMapTex: texture(textureSet.normalMap ?? fallbackEnvironmentNormalTexture),
178
+ normalMapStrength: uniform(0.8),
179
+ normalMapScale: uniform(textureSet.normalScale ?? new THREE.Vector2(1, 1)),
180
+ aoMapTex: texture(textureSet.aoMap ?? fallbackEnvironmentWhiteTexture),
181
+ aoMapStrength: uniform(THREE.MathUtils.clamp(textureSet.aoMapIntensity ?? 1, 0, 2)),
182
+ aoWarmth: uniform(0.55),
183
+ lightMapTex: texture(textureSet.lightMap ?? fallbackEnvironmentBlackTexture),
184
+ lightMapStrength: uniform(THREE.MathUtils.clamp(textureSet.lightMapIntensity ?? 1, 0, 2)),
185
+ lightMapLift: uniform(0.1),
186
+ emissiveMapTex: texture(textureSet.emissiveMap ?? fallbackEnvironmentBlackTexture),
187
+ emissiveMapColor: uniform(textureSet.emissiveColor ?? new THREE.Color(1, 1, 1)),
188
+ // Sunlight dominates emissives by day; lamps carry the room at night.
189
+ emissiveMapStrength: uniform(hasSun ? 0.25 : 1.0),
190
+ vertexAoStrength: uniform(0.85),
191
+ untexturedGradientStrength: uniform(0.4),
192
+ specularStrength: uniform(isGlossFloor ? 0.35 : 0.0),
193
+ specularShininess: uniform(isGlossFloor ? 48 : 32),
194
+ specularSoftness: uniform(0.22),
195
+ specularColor: uniform(new THREE.Color(1, 1, 1)),
196
+ interiorOcclusionStrength: uniform(0.0),
197
+ interiorOcclusionColor: uniform(new THREE.Color(0.34, 0.29, 0.25)),
198
+ ambientProbeBlend: uniform(0.0),
199
+ heightFogDensity: uniform(0.0),
200
+ heightFogFalloff: uniform(6.0),
201
+ heightFogColor: uniform(new THREE.Color(0.75, 0.82, 0.92)),
202
+ planarReflectionStrength: uniform(isGlossFloor ? 0.3 : 0.0),
203
+ planarReflectionFresnel: uniform(2.4),
204
+
205
+ enableAlphaMap: uniform(1.0),
206
+ enableAlphaCutout: uniform(1.0),
207
+ enableFoliageCutout: uniform(1.0),
208
+ enablePackedMap: uniform(1.0),
209
+ enableWindowCutout: uniform(1.0),
210
+ enableDirectionalLights: uniform(1.0),
211
+ enablePointLights: uniform(1.0),
212
+ enableSpotLights: uniform(1.0),
213
+ enableShadowMask: uniform(1.0),
214
+ enableAmbientLight: uniform(1.0),
215
+ enableSkyTint: uniform(1.0),
216
+ enableLeftSideShadow: uniform(1.0),
217
+ enableEmissive: uniform(1.0),
218
+ enableSunBoost: uniform(1.0),
219
+ enableNormalMap: uniform(1.0),
220
+ enableAoMap: uniform(1.0),
221
+ enableLightMap: uniform(1.0),
222
+ enableVertexAo: uniform(1.0),
223
+ enableUntexturedGradient: uniform(1.0),
224
+ enableSpecular: uniform(1.0),
225
+ enableInteriorOcclusion: uniform(1.0),
226
+ enableAmbientProbe: uniform(1.0),
227
+ enableHeightFog: uniform(1.0),
228
+ enablePlanarReflection: uniform(1.0),
229
+
230
+ // Scene-wide state, shared by reference (the classic factory swaps these
231
+ // slots for the shared uniform objects the same way).
232
+ time: shared.time,
233
+ cloudShadowStrength: shared.cloudShadowStrength,
234
+ cloudShadowCoverage: shared.cloudShadowCoverage,
235
+ cloudShadowScale: shared.cloudShadowScale,
236
+ cloudShadowVelocity: shared.cloudShadowVelocity,
237
+ envDebugMode: shared.envDebugMode,
238
+ environmentOpenings: shared.environmentOpenings,
239
+ environmentOpeningCount: shared.environmentOpeningCount,
240
+ ambientProbe: shared.ambientProbe,
241
+ planarReflectionMap: shared.planarReflectionMap,
242
+ planarReflectionMatrix: shared.planarReflectionMatrix,
243
+ };
244
+
245
+ // Texture nodes referenced by the graph — only flag-gated maps are ever
246
+ // sampled, so absent maps never claim a texture unit.
247
+ const tex = {
248
+ alphaMap: u.alphaMap,
249
+ aoMapTex: u.aoMapTex,
250
+ baseMap: u.baseMap,
251
+ emissiveMapTex: u.emissiveMapTex,
252
+ lightMapTex: u.lightMapTex,
253
+ normalMapTex: u.normalMapTex,
254
+ packedMap: u.packedMap,
255
+ planarReflectionMap: shared.planarReflectionMap,
256
+ };
257
+
258
+ const material = new NodeMaterial();
259
+ material.name = 'ToonEnvironmentNode';
260
+ material.lights = false;
261
+ // TODO(tsl Phase 7): fog ordering — the GLSL applies scene fog AFTER the
262
+ // colorspace conversion (fog_fragment follows colorspace_fragment); the
263
+ // node pipeline would apply it before. Scene fog is disabled here until the
264
+ // fog phase resolves the ordering (the Phase 3 gate scene has no scene.fog).
265
+ material.fog = false;
266
+ material.side = side;
267
+ material.transparent = alphaBlend;
268
+ material.depthWrite = !alphaBlend;
269
+ material.alphaTest = 0; // alpha cutout is in-graph (alphaCutoff), like the GLSL
270
+ material.vertexColors = Boolean(flags.useVertexColors);
271
+ // Debug views are compiled in on this backend; the marker keeps
272
+ // setEnvironmentDebugOutput's define-flip loop from forcing a recompile.
273
+ material.defines = { ENV_DEBUG_VIEWS: 1 };
274
+
275
+ // ---- Vertex stage ----
276
+ // Varyings are declared up front and assigned imperatively inside the
277
+ // single vertexNode Fn (docs/tsl-conventions.md gotcha 4).
278
+ const vUv = varying(vec2(), 'vEnvUv');
279
+ const vUv2 = flags.useUv2 ? varying(vec2(), 'vEnvUv2') : null;
280
+ const vEnvVertexAo = flags.hasVertexAo ? varying(float(), 'vEnvVertexAo') : null;
281
+ const vWorldNormal = varying(vec3(), 'vEnvWorldNormal');
282
+ const vWorldPosition = varying(vec3(), 'vEnvWorldPosition');
283
+ const vViewPosition = varying(vec3(), 'vEnvViewPosition');
284
+ const vRoomUv = varying(vec2(), 'vEnvRoomUv');
285
+
286
+ material.vertexNode = Fn(() => {
287
+ vUv.assign(u.baseMapTransform.mul(vec3(uv(), 1.0)).xy);
288
+ if (flags.useUv2) vUv2.assign(attribute('uv1', 'vec2'));
289
+ if (flags.hasVertexAo) vEnvVertexAo.assign(attribute('envVertexAo', 'float'));
290
+
291
+ // GLSL: vWorldNormal = normalize(transformedNormal) — three's VIEW-space
292
+ // normal (see the quirk note in the header).
293
+ vWorldNormal.assign(normalize(transformNormalToView(normalLocal)));
294
+
295
+ const worldPosition = positionWorld.toVar();
296
+ vWorldPosition.assign(worldPosition);
297
+ // Room-normalized position for the side-shadow gradient; linear in world
298
+ // space, so interpolating it is exact and the fragment stage skips the
299
+ // per-pixel divide.
300
+ vRoomUv.assign(
301
+ worldPosition.sub(u.environmentCenter)
302
+ .div(max(u.environmentSize, vec3(0.001))).xy.add(0.5),
303
+ );
304
+
305
+ const mvPosition = cameraViewMatrix.mul(vec4(worldPosition, 1.0));
306
+ vViewPosition.assign(mvPosition.xyz.negate());
307
+ return cameraProjectionMatrix.mul(mvPosition);
308
+ })();
309
+
310
+ const shadowSideFor = {
311
+ [THREE.FrontSide]: THREE.BackSide,
312
+ [THREE.BackSide]: THREE.FrontSide,
313
+ [THREE.DoubleSide]: THREE.DoubleSide,
314
+ };
315
+
316
+ material.userData.createDepthColorVariant = () => {
317
+ const depthMaterial = new NodeMaterial();
318
+ depthMaterial.name = `${material.name || 'ToonEnvironmentNode'}Depth`;
319
+ depthMaterial.lights = false;
320
+ depthMaterial.fog = false;
321
+ depthMaterial.side = material.shadowSide ?? shadowSideFor[material.side] ?? THREE.DoubleSide;
322
+ depthMaterial.transparent = false;
323
+ depthMaterial.depthWrite = true;
324
+ depthMaterial.depthTest = material.depthTest;
325
+ depthMaterial.vertexColors = material.vertexColors;
326
+ depthMaterial.isShadowPassMaterial = true;
327
+ depthMaterial.vertexNode = material.vertexNode;
328
+ depthMaterial.fragmentNode = Fn(() => {
329
+ const texel = tex.baseMap.sample(vUv).toVar();
330
+ if (flags.useVertexColors) {
331
+ const vColor = vertexColor();
332
+ texel.a.mulAssign(vColor.a);
333
+ }
334
+
335
+ Discard(
336
+ u.enableFoliageCutout.greaterThan(0.5)
337
+ .and(u.foliageCutout.greaterThan(0.5))
338
+ .and(max3(texel.rgb).lessThan(0.11)),
339
+ );
340
+ const paneMask = windowPaneMask(texel.rgb).toVar();
341
+ Discard(
342
+ u.enableWindowCutout.greaterThan(0.5)
343
+ .and(u.windowCutout.greaterThan(0.5))
344
+ .and(paneMask.greaterThan(0.44)),
345
+ );
346
+
347
+ const alpha = texel.a.mul(u.opacity).toVar();
348
+ if (flags.hasAlphaMap) {
349
+ If(u.enableAlphaMap.greaterThan(0.5).and(u.useAlphaMap.greaterThan(0.5)), () => {
350
+ const alphaTexel = tex.alphaMap.sample(vUv).toVar();
351
+ const textureAlpha = select(
352
+ u.alphaFromLuminance.greaterThan(0.5),
353
+ max3(alphaTexel.rgb),
354
+ alphaTexel.a,
355
+ );
356
+ alpha.mulAssign(textureAlpha);
357
+ });
358
+ }
359
+ Discard(
360
+ u.enableAlphaCutout.greaterThan(0.5)
361
+ .and(u.alphaCutoff.greaterThanEqual(0.0))
362
+ .and(alpha.lessThan(u.alphaCutoff)),
363
+ );
364
+
365
+ const orthographic = cameraProjectionMatrix.element(3).w.equal(1.0);
366
+ const viewZ = vViewPosition.z.negate();
367
+ const depth01 = select(
368
+ orthographic,
369
+ viewZToOrthographicDepth(viewZ, cameraNear, cameraFar),
370
+ viewZToPerspectiveDepth(viewZ, cameraNear, cameraFar),
371
+ );
372
+ return vec4(vec3(depth01), 1.0);
373
+ })();
374
+ return depthMaterial;
375
+ };
376
+
377
+ // ---- Fragment stage ----
378
+ const lighting = createEnvironmentLightingChunk({
379
+ cameraViewMatrixNode: cameraViewMatrix,
380
+ flags,
381
+ tex,
382
+ u,
383
+ });
384
+
385
+ // #ifdef ENV_AO_UV2 / ENV_LIGHT_UV2 — compile-time uv channel selection.
386
+ const envAoUv = flags.aoUsesUv2 && vUv2 ? vUv2 : vUv;
387
+ const envLightUv = flags.lightUsesUv2 && vUv2 ? vUv2 : vUv;
388
+
389
+ material.fragmentNode = Fn(() => {
390
+ const texel = tex.baseMap.sample(vUv).toVar();
391
+ if (flags.useVertexColors) {
392
+ // USE_COLOR / USE_COLOR_ALPHA: vertexColor() yields w = 1 for vec3
393
+ // color attributes, so the alpha multiply is a no-op exactly when the
394
+ // classic pipeline compiled without USE_COLOR_ALPHA.
395
+ const vColor = vertexColor();
396
+ texel.rgb.mulAssign(vColor.rgb);
397
+ texel.a.mulAssign(vColor.a);
398
+ }
399
+
400
+ Discard(
401
+ u.enableFoliageCutout.greaterThan(0.5)
402
+ .and(u.foliageCutout.greaterThan(0.5))
403
+ .and(max3(texel.rgb).lessThan(0.11)),
404
+ );
405
+ const paneMask = windowPaneMask(texel.rgb).toVar();
406
+ Discard(
407
+ u.enableWindowCutout.greaterThan(0.5)
408
+ .and(u.windowCutout.greaterThan(0.5))
409
+ .and(paneMask.greaterThan(0.44)),
410
+ );
411
+
412
+ const alpha = texel.a.mul(u.opacity).toVar();
413
+ if (flags.hasAlphaMap) {
414
+ If(u.enableAlphaMap.greaterThan(0.5).and(u.useAlphaMap.greaterThan(0.5)), () => {
415
+ const alphaTexel = tex.alphaMap.sample(vUv).toVar();
416
+ const textureAlpha = select(
417
+ u.alphaFromLuminance.greaterThan(0.5),
418
+ max3(alphaTexel.rgb),
419
+ alphaTexel.a,
420
+ );
421
+ alpha.mulAssign(textureAlpha);
422
+ });
423
+ }
424
+ Discard(
425
+ u.enableAlphaCutout.greaterThan(0.5)
426
+ .and(u.alphaCutoff.greaterThanEqual(0.0))
427
+ .and(alpha.lessThan(u.alphaCutoff)),
428
+ );
429
+
430
+ const albedo = pow(max(texel.rgb.mul(u.baseColor), vec3(0.0)), vec3(0.92)).toVar();
431
+
432
+ const N = normalize(vWorldNormal)
433
+ .mul(select(frontFacing, float(1.0), float(-1.0)))
434
+ .toVar();
435
+ if (flags.hasNormalMap) {
436
+ If(u.enableNormalMap.greaterThan(0.5).and(u.normalMapStrength.greaterThan(0.0)), () => {
437
+ N.assign(lighting.perturbEnvironmentNormal(N, vWorldPosition, vUv));
438
+ });
439
+ }
440
+
441
+ const upward = N.y.mul(0.5).add(0.5);
442
+ const coolSkyTint = mix(u.skyGroundTint, u.skyTopTint, upward);
443
+
444
+ if (flags.hasUntextured) {
445
+ // Designed flat-color response: walls fall off gently toward the floor
446
+ // and up-facing surfaces pick up a hint of sky.
447
+ If(u.enableUntexturedGradient.greaterThan(0.5), () => {
448
+ const heightT = clamp(vRoomUv.y, 0.0, 1.0);
449
+ const floorDarkening = u.untexturedGradientStrength.mul(0.5)
450
+ .mul(smoothstep(0.0, 0.55, heightT).oneMinus())
451
+ .oneMinus();
452
+ albedo.mulAssign(floorDarkening);
453
+ albedo.mulAssign(mix(vec3(1.0), coolSkyTint, u.untexturedGradientStrength.mul(0.35)));
454
+ });
455
+ }
456
+
457
+ const directional = lighting.evaluateDirectionalLight(N);
458
+ const directLight = directional.light.mul(u.directLightStrength).toVar();
459
+ const strongestLight = directional.strongest;
460
+
461
+ // getShadowMask() → shared sun-shadow sampler, fed by
462
+ // environmentSunShadowPass on the node backends (mask = 1 until it runs).
463
+ // The Fn call is hoisted to statement level per the conventions doc's
464
+ // hoisting guidance (docs/tsl-conventions.md, select()/If call hazards);
465
+ // the enableShadowMask ternary becomes an If()-assigned var.
466
+ const sampledSunShadow = sampleEnvironmentSunShadow(vWorldPosition).toVar();
467
+ const sunlightVisibility = float(1.0).toVar();
468
+ If(u.enableShadowMask.greaterThan(0.5), () => {
469
+ sunlightVisibility.assign(sampledSunShadow);
470
+ });
471
+ // Drifting procedural cloud shadows over outdoor terrain; strength
472
+ // defaults to 0 so indoor scenes are untouched.
473
+ sunlightVisibility.mulAssign(stylizedCloudShadow(
474
+ vWorldPosition.xz, u.time,
475
+ u.cloudShadowStrength, u.cloudShadowCoverage, u.cloudShadowScale, u.cloudShadowVelocity,
476
+ ));
477
+ directLight.mulAssign(sunlightVisibility);
478
+ strongestLight.mulAssign(sunlightVisibility);
479
+
480
+ const geometryPosition = vViewPosition.negate(); // GLSL passes -vViewPosition
481
+ const point = lighting.evaluatePointLights(N, geometryPosition);
482
+ const pointLight = point.light.mul(u.pointLightStrength).toVar();
483
+ const spot = lighting.evaluateSpotLights(N, geometryPosition);
484
+ const spotLight = spot.light.mul(u.spotLightStrength).toVar();
485
+ strongestLight.assign(max(strongestLight, point.strongest.mul(0.35)));
486
+ strongestLight.assign(max(strongestLight, spot.strongest.mul(0.35)));
487
+
488
+ const ambientBase = mix(vec3(1.0), toonSceneLights.ambientLightColor, u.ambientLightInfluence).toVar();
489
+ // Ambient probe reshapes ambient color toward the room's own palette;
490
+ // ambientStrength still owns the magnitude.
491
+ If(u.enableAmbientProbe.greaterThan(0.5).and(u.ambientProbeBlend.greaterThan(0.0)), () => {
492
+ ambientBase.assign(mix(ambientBase, lighting.environmentProbeIrradiance(N), u.ambientProbeBlend));
493
+ });
494
+ const ambient = ambientBase.mul(u.ambientStrength).mul(u.enableAmbientLight).toVar();
495
+ ambient.mulAssign(mix(vec3(1.0), coolSkyTint, u.skyTintStrength.mul(u.enableSkyTint)));
496
+
497
+ const dbgBakedGi = vec3(0.0).toVar();
498
+ if (flags.hasLightMap) {
499
+ // Baked GI replaces the flat ambient where a lightmap exists.
500
+ If(u.enableLightMap.greaterThan(0.5).and(u.lightMapStrength.greaterThan(0.0)), () => {
501
+ const baked = tex.lightMapTex.sample(envLightUv).rgb.mul(u.lightMapStrength).toVar();
502
+ baked.assign(mix(u.shadowTintColor.mul(baked), baked, smoothstep(0.0, 0.62, envLuma(baked))));
503
+ baked.assign(max(baked, vec3(u.lightMapLift)));
504
+ dbgBakedGi.assign(baked);
505
+ ambient.assign(mix(ambient, baked, clamp(u.lightMapStrength, 0.0, 1.0)));
506
+ strongestLight.assign(max(strongestLight, smoothstep(0.55, 1.0, envLuma(baked))));
507
+ });
508
+ }
509
+
510
+ // Baked occlusion multiplies indirect light only, tinted warm so shade
511
+ // reads painted rather than gray.
512
+ const aoMul = vec3(1.0).toVar();
513
+ const warmOcclusionFloor = mix(vec3(0.0), u.shadowTintColor, u.aoWarmth);
514
+ const dbgVertexAo = float(1.0).toVar();
515
+ if (flags.hasAoMap) {
516
+ If(u.enableAoMap.greaterThan(0.5).and(u.aoMapStrength.greaterThan(0.0)), () => {
517
+ const aoSample = tex.aoMapTex.sample(envAoUv).r;
518
+ const ao = mix(1.0, aoSample, u.aoMapStrength);
519
+ aoMul.mulAssign(mix(warmOcclusionFloor, vec3(1.0), ao));
520
+ });
521
+ }
522
+ if (flags.hasVertexAo) {
523
+ If(u.enableVertexAo.greaterThan(0.5).and(u.vertexAoStrength.greaterThan(0.0)), () => {
524
+ dbgVertexAo.assign(vEnvVertexAo);
525
+ const vao = mix(1.0, vEnvVertexAo, u.vertexAoStrength);
526
+ aoMul.mulAssign(mix(warmOcclusionFloor, vec3(1.0), vao));
527
+ });
528
+ }
529
+ ambient.mulAssign(aoMul);
530
+
531
+ const litColor = albedo.mul(ambient.add(directLight).add(pointLight).add(spotLight)).toVar();
532
+
533
+ const dbgSpecular = vec3(0.0).toVar();
534
+ If(u.enableSpecular.greaterThan(0.5).and(u.specularStrength.greaterThan(0.0)), () => {
535
+ dbgSpecular.assign(
536
+ lighting.evaluateEnvironmentSpecular(N, geometryPosition, sunlightVisibility)
537
+ .mul(u.specularStrength).mul(u.specularColor),
538
+ );
539
+ litColor.addAssign(dbgSpecular);
540
+ });
541
+
542
+ const color = mix(albedo, litColor, u.lightingInfluence).toVar();
543
+ const sunBand = smoothstep(0.45, 0.9, strongestLight);
544
+ color.addAssign(albedo.mul(u.sunBoostColor).mul(sunBand).mul(u.sunBoost).mul(u.enableSunBoost));
545
+
546
+ // Legacy Liyue-tuned left-edge gradient; superseded by openings-based
547
+ // interior occlusion but preserved for the approved room look.
548
+ const leftDarkness = smoothstep(0.16, 0.72, vRoomUv.x).oneMinus()
549
+ .mul(smoothstep(0.78, 1.08, vRoomUv.y).oneMinus())
550
+ .toVar();
551
+ color.mulAssign(mix(
552
+ vec3(1.0),
553
+ u.leftSideShadowColor,
554
+ leftDarkness.mul(u.leftSideShadow).mul(u.enableLeftSideShadow),
555
+ ));
556
+
557
+ // Interiors darken away from registered openings; inert with no openings.
558
+ const dbgInteriorDark = float(0.0).toVar();
559
+ If(
560
+ u.enableInteriorOcclusion.greaterThan(0.5)
561
+ .and(u.interiorOcclusionStrength.greaterThan(0.0))
562
+ .and(u.environmentOpeningCount.greaterThan(0.5)),
563
+ () => {
564
+ const openingLight = float(0.0).toVar();
565
+ for (let i = 0; i < 4; i += 1) {
566
+ If(float(i).lessThan(u.environmentOpeningCount), () => {
567
+ const opening = u.environmentOpenings.element(i);
568
+ const reach = max(opening.w, 0.001);
569
+ const dist = distance(vWorldPosition, opening.xyz);
570
+ openingLight.assign(max(
571
+ openingLight,
572
+ smoothstep(reach.mul(0.35), reach.mul(1.45), dist).oneMinus(),
573
+ ));
574
+ });
575
+ }
576
+ dbgInteriorDark.assign(openingLight.oneMinus().mul(u.interiorOcclusionStrength));
577
+ color.mulAssign(mix(vec3(1.0), u.interiorOcclusionColor, dbgInteriorDark));
578
+ },
579
+ );
580
+
581
+ if (flags.hasPackedMap) {
582
+ If(u.enablePackedMap.greaterThan(0.5).and(u.usePackedMap.greaterThan(0.5)), () => {
583
+ const packed = tex.packedMap.sample(vUv).rgb.toVar();
584
+ const packedBrightness = max3(packed);
585
+ const materialOcclusion = mix(1.0, clamp(packed.r.mul(1.18), 0.62, 1.08), u.packedOcclusionStrength);
586
+ const bakedGlow = smoothstep(0.62, 0.96, max(packed.r, packed.g));
587
+ color.mulAssign(materialOcclusion);
588
+ color.addAssign(albedo.mul(bakedGlow).mul(u.bakedGlowStrength));
589
+ strongestLight.assign(max(strongestLight, smoothstep(0.58, 1.0, packedBrightness).mul(0.58)));
590
+ });
591
+ }
592
+
593
+ const liftedShadow = max(strongestLight, u.shadowLift);
594
+ color.mulAssign(mix(u.shadowTintColor, vec3(1.0), liftedShadow));
595
+
596
+ const emissiveMask = float(0.0).toVar();
597
+ if (flags.hasEmissiveMap) {
598
+ // A real emissive map replaces the bright-albedo luminance guess.
599
+ const emissiveTexel = tex.emissiveMapTex.sample(vUv).rgb
600
+ .mul(u.emissiveMapColor).mul(u.emissiveMapStrength);
601
+ color.addAssign(emissiveTexel.mul(u.enableEmissive));
602
+ emissiveMask.assign(envLuma(emissiveTexel));
603
+ } else {
604
+ emissiveMask.assign(smoothstep(0.68, 1.0, max3(albedo)));
605
+ color.addAssign(albedo.mul(emissiveMask).mul(u.emissiveStrength).mul(u.enableEmissive));
606
+ emissiveMask.mulAssign(u.emissiveStrength);
607
+ }
608
+
609
+ if (flags.hasPlanarReflection) {
610
+ // Mirrored-scene reflection for glossy floors; fresnel keeps it
611
+ // strongest at grazing angles like lacquer.
612
+ If(u.enablePlanarReflection.greaterThan(0.5).and(u.planarReflectionStrength.greaterThan(0.0)), () => {
613
+ const reflectionUv = u.planarReflectionMatrix.mul(vec4(vWorldPosition, 1.0));
614
+ // texture2DProj(map, uv4) — divide by w; RT-fed textures sample at
615
+ // explicit level 0 (docs/tsl-conventions.md gotcha 9).
616
+ const reflection = tex.planarReflectionMap
617
+ .sample(reflectionUv.xy.div(reflectionUv.w)).level(0).rgb;
618
+ const toCamera = normalize(cameraPosition.sub(vWorldPosition));
619
+ const fresnel = pow(
620
+ clamp(dot(N, toCamera), 0.0, 1.0).oneMinus(),
621
+ max(u.planarReflectionFresnel, 0.001),
622
+ );
623
+ color.assign(mix(
624
+ color,
625
+ reflection,
626
+ clamp(u.planarReflectionStrength.mul(mix(0.16, 1.0, fresnel)), 0.0, 1.0),
627
+ ));
628
+ });
629
+ }
630
+
631
+ color.assign(applySaturation(color.mul(u.exposure), u.saturation));
632
+
633
+ // World-height fog: dense near the environment floor, fading with height.
634
+ If(u.enableHeightFog.greaterThan(0.5).and(u.heightFogDensity.greaterThan(0.0)), () => {
635
+ const floorY = u.environmentCenter.y.sub(u.environmentSize.y.mul(0.5));
636
+ const heightFalloff = exp(
637
+ max(vWorldPosition.y.sub(floorY), 0.0).div(max(u.heightFogFalloff, 0.001)).negate(),
638
+ );
639
+ const depthTerm = exp(length(vViewPosition).mul(u.heightFogDensity).negate()).oneMinus();
640
+ color.assign(mix(color, u.heightFogColor, clamp(depthTerm.mul(heightFalloff), 0.0, 1.0)));
641
+ });
642
+
643
+ const finalColor = vec4(max(color, vec3(0.0)), alpha);
644
+
645
+ // Debug table (masked sum). envDebugMode 0 matches no entry, so the
646
+ // result is exactly finalColor during normal rendering — the ENV_DEBUG_VIEWS
647
+ // define flip becomes a pure uniform write on this backend.
648
+ return environmentDebugColor({
649
+ albedo,
650
+ alpha,
651
+ ambient,
652
+ aoMul,
653
+ bakedGi: dbgBakedGi,
654
+ directLight,
655
+ emissiveMask,
656
+ finalColor,
657
+ litColor,
658
+ mode: u.envDebugMode,
659
+ normal: N,
660
+ pointLight,
661
+ roomOcclusion: clamp(
662
+ dbgInteriorDark.add(leftDarkness.mul(u.leftSideShadow).mul(u.enableLeftSideShadow)),
663
+ 0.0,
664
+ 1.0,
665
+ ),
666
+ specular: dbgSpecular,
667
+ spotLight,
668
+ sunlightVisibility,
669
+ vertexAo: dbgVertexAo,
670
+ windowMask: paneMask,
671
+ });
672
+ })();
673
+
674
+ // Same-name uniform slots (ShaderMaterial-compatible `.value` access).
675
+ material.uniforms = u;
676
+ material.userData.isEnvironmentNodeMaterial = true;
677
+ material.userData.environmentTslFlags = { ...flags };
678
+
679
+ return material;
680
+ }