@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,64 @@
1
+ // TSL port of src/shaders/chunks/character-fragment-color.glsl — albedo color
2
+ // edits and HSV helpers shared by the toon character shader.
3
+ //
4
+ // GLSL's applyHSVChange returns the edited color and its original HSV through
5
+ // an out-parameter; here the two are separate calls (rgbToHsv is cheap and the
6
+ // builder de-duplicates identical subgraphs).
7
+
8
+ import {
9
+ abs,
10
+ clamp,
11
+ dot,
12
+ float,
13
+ Fn,
14
+ fract,
15
+ max,
16
+ min,
17
+ mix,
18
+ pow,
19
+ select,
20
+ step,
21
+ vec3,
22
+ vec4,
23
+ } from 'three/tsl';
24
+
25
+ export const LUMA = vec3(0.2126, 0.7152, 0.0722);
26
+
27
+ export const maxColorComponent = /*@__PURE__*/ Fn(([value]) => {
28
+ return max(value.r, max(value.g, value.b));
29
+ });
30
+
31
+ export const rgbToHsv = /*@__PURE__*/ Fn(([c]) => {
32
+ const K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
33
+ const p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)).toVar();
34
+ const q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)).toVar();
35
+ const d = q.x.sub(min(q.w, q.y));
36
+ const e = float(1.0e-10);
37
+ return vec3(
38
+ abs(q.z.add(q.w.sub(q.y).div(d.mul(6.0).add(e)))),
39
+ d.div(q.x.add(e)),
40
+ q.x,
41
+ );
42
+ });
43
+
44
+ export const hsvToRgb = /*@__PURE__*/ Fn(([c]) => {
45
+ const p = abs(fract(c.xxx.add(vec3(0.0, 2.0 / 3.0, 1.0 / 3.0))).mul(6.0).sub(3.0));
46
+ return c.z.mul(mix(vec3(1.0), clamp(p.sub(1.0), 0.0, 1.0), c.y));
47
+ });
48
+
49
+ // GLSL applyHSVChange without the originalHSV out-param — callers that need
50
+ // the original HSV call rgbToHsv themselves.
51
+ export const applyHSVChange = /*@__PURE__*/ Fn(([color, hueOffset, saturationBoost, valueMul]) => {
52
+ const hsv = rgbToHsv(max(color, vec3(0.0))).toVar();
53
+ hsv.x.assign(fract(hsv.x.add(hueOffset)));
54
+ hsv.y.assign(clamp(hsv.y.add(saturationBoost), 0.0, 1.0));
55
+ hsv.z.mulAssign(valueMul);
56
+ return hsvToRgb(hsv);
57
+ });
58
+
59
+ export const adjustSaturation = /*@__PURE__*/ Fn(([color, saturation]) => {
60
+ const luma = dot(color, LUMA);
61
+ const adjusted = mix(vec3(luma), color, max(saturation, 0.0));
62
+ // GLSL early-outs when |saturation - 1| < 1e-4; same result via select.
63
+ return select(abs(saturation.sub(1.0)).lessThan(0.0001), color, adjusted);
64
+ });
@@ -0,0 +1,174 @@
1
+ // TSL port of src/shaders/chunks/character-fragment-highlights.glsl —
2
+ // specular band, hair strand highlight, eye gloss, and procedural glitter.
3
+
4
+ import {
5
+ abs,
6
+ clamp,
7
+ dot,
8
+ float,
9
+ Fn,
10
+ fract,
11
+ If,
12
+ length,
13
+ max,
14
+ min,
15
+ mix,
16
+ normalize,
17
+ pow,
18
+ reflect,
19
+ select,
20
+ smoothstep,
21
+ sqrt,
22
+ vec2,
23
+ vec3,
24
+ } from 'three/tsl';
25
+
26
+ function maskChannel(maskTexel, channelUniform) {
27
+ return select(channelUniform.equal(1), maskTexel.g,
28
+ select(channelUniform.equal(2), maskTexel.b,
29
+ select(channelUniform.equal(3), maskTexel.a, maskTexel.r)));
30
+ }
31
+
32
+ export function createHighlightsChunk({ u, tex, v, flags, toonEdgeSmooth }) {
33
+ const sampleSpecularMask = () => {
34
+ if (!flags.hasSpecularMask) return float(1.0);
35
+ const maskTexel = tex.specularMaskMap.sample(v.vUv);
36
+ const mask = maskChannel(maskTexel, u.specularMaskChannel);
37
+ return select(
38
+ u.useSpecularMask,
39
+ mix(1.0, clamp(mask, 0.0, 1.0), u.specularMaskStrength),
40
+ 1.0,
41
+ );
42
+ };
43
+
44
+ const calculateSpecularArea = (NoH, NoV, finalShadowArea, roughnessValue) => {
45
+ const roughnessPower = mix(192.0, 8.0, clamp(roughnessValue, 0.0, 1.0));
46
+ const resolvedPower = mix(u.specularPower, roughnessPower, u.materialRoughnessStrength);
47
+ // View mode anchors the highlight to the surface instead of the light.
48
+ const specularSource = select(u.specularDirectionMode.equal(1), NoV, NoH);
49
+ const specularRaw = pow(specularSource, max(resolvedPower, 1.0));
50
+ const specStart = max(0.0, u.specularAreaRemapMidPoint.sub(u.specularAreaRemapRange));
51
+ const specEnd = min(1.0, u.specularAreaRemapMidPoint.add(u.specularAreaRemapRange));
52
+ const specularArea = toonEdgeSmooth(specularRaw, specStart, specEnd);
53
+ const specularShadowMask = mix(finalShadowArea, 1.0, u.specularShowInShadowArea);
54
+ const roughnessMask = mix(1.0, roughnessValue.mul(0.65).oneMinus(), u.materialRoughnessStrength);
55
+ const result = specularArea.mul(specularShadowMask).mul(roughnessMask).mul(sampleSpecularMask());
56
+ return select(u.useSpecular, result, 0.0);
57
+ };
58
+
59
+ const sampleHairHighlightMask = (uv) => {
60
+ if (!flags.hasHairHighlightMask) return float(1.0);
61
+ const maskTexel = tex.hairHighlightMaskMap.sample(uv);
62
+ const mask = maskChannel(maskTexel, u.hairHighlightMaskChannel);
63
+ return select(
64
+ u.useHairHighlightMask,
65
+ mix(1.0, clamp(mask, 0.0, 1.0), u.hairHighlightMaskStrength),
66
+ 1.0,
67
+ );
68
+ };
69
+
70
+ const sampleEyeHighlightMask = (uv) => {
71
+ if (!flags.hasEyeHighlightMask) return float(1.0);
72
+ const maskTexel = tex.eyeHighlightMaskMap.sample(uv);
73
+ const mask = maskChannel(maskTexel, u.eyeHighlightMaskChannel);
74
+ return select(
75
+ u.useEyeHighlightMask,
76
+ mix(1.0, clamp(mask, 0.0, 1.0), u.eyeHighlightMaskStrength),
77
+ 1.0,
78
+ );
79
+ };
80
+
81
+ const calculateHairHighlightMask = (V, N, H, uv, finalShadowArea) => {
82
+ const strandDirection = normalize(u.hairHighlightDirection);
83
+ const tangentHalf = dot(strandDirection, H);
84
+ const strandTangent = pow(
85
+ clamp(sqrt(max(0.0, tangentHalf.mul(tangentHalf).oneMinus())), 0.0, 1.0),
86
+ u.hairHighlightStrandPower,
87
+ );
88
+ const strandReflect = pow(
89
+ clamp(abs(dot(strandDirection, reflect(V.negate(), N))).oneMinus(), 0.0, 1.0),
90
+ u.hairHighlightStrandPower,
91
+ );
92
+ const strand = select(u.hairHighlightMode.equal(1), strandTangent, strandReflect);
93
+
94
+ const uvCoord = select(u.hairHighlightUvBandAxis.equal(1), uv.y, uv.x);
95
+ const sideBand = pow(
96
+ clamp(
97
+ abs(uvCoord.sub(u.hairHighlightUvBandCenter)).div(max(u.hairHighlightUvBandHalfWidth, 0.001)).oneMinus(),
98
+ 0.0,
99
+ 1.0,
100
+ ),
101
+ u.hairHighlightSideBandPower,
102
+ );
103
+ const result = strand.mul(sideBand)
104
+ .mul(sampleHairHighlightMask(uv))
105
+ .mul(mix(u.hairHighlightShadowFloor, 1.0, finalShadowArea));
106
+ return select(u.useHairHighlight, result, 0.0);
107
+ };
108
+
109
+ const calculateEyeHighlightMask = (L, N, V, uv) => {
110
+ const gloss = pow(clamp(dot(reflect(L.negate(), N), V), 0.0, 1.0), max(u.eyeHighlightPower, 1.0));
111
+ return select(u.useEyeHighlight, gloss.mul(sampleEyeHighlightMask(uv)), 0.0);
112
+ };
113
+
114
+ // Procedural glitter — one particle per UV cell with a random orientation,
115
+ // lit by a very tight reflection lobe. Only built when glitter is enabled
116
+ // on the material (GLSL: USE_TOON_GLITTER define).
117
+ const toonGlitterHash = /*@__PURE__*/ Fn(([p]) => {
118
+ const p3 = fract(vec3(p.x, p.y, p.x).mul(0.1031)).toVar();
119
+ p3.addAssign(dot(p3, p3.yzx.add(33.33)));
120
+ return fract(p3.x.add(p3.y).mul(p3.z));
121
+ });
122
+
123
+ const toonGlitterHash2 = /*@__PURE__*/ Fn(([p]) => {
124
+ const p3 = fract(vec3(p.x, p.y, p.x).mul(vec3(0.1031, 0.103, 0.0973))).toVar();
125
+ p3.addAssign(dot(p3, p3.yzx.add(33.33)));
126
+ return fract(p3.xx.add(p3.yz).mul(p3.zy));
127
+ });
128
+
129
+ const toonGlitterHash3 = /*@__PURE__*/ Fn(([p]) => {
130
+ const p3 = fract(vec3(p.x, p.y, p.x).mul(vec3(0.1031, 0.103, 0.0973))).toVar();
131
+ p3.addAssign(dot(p3, p3.yxz.add(33.33)));
132
+ return fract(p3.xxy.add(p3.yzz).mul(p3.zyx)).mul(2.0).sub(1.0);
133
+ });
134
+
135
+ const evaluateGlitter = (uv, V, N, L) => {
136
+ const particleSize = u.glitterSize.mul(0.25).toVar();
137
+ const scaledUv = uv.mul(u.glitterDensity).mul(1000.0).toVar();
138
+ const cellId = scaledUv.floor().toVar();
139
+ const cellUv = fract(scaledUv).toVar();
140
+ const glitter = vec3(0.0).toVar();
141
+
142
+ for (let y = -1; y <= 1; y += 1) {
143
+ for (let x = -1; x <= 1; x += 1) {
144
+ const neighborCell = cellId.add(vec2(x, y)).toVar();
145
+ const particlePos = vec2(x, y).add(toonGlitterHash2(neighborCell));
146
+ const dist = length(particlePos.sub(cellUv)).toVar();
147
+ If(dist.lessThan(particleSize), () => {
148
+ const particleNormal = normalize(mix(
149
+ N,
150
+ normalize(toonGlitterHash3(neighborCell)),
151
+ u.glitterRandomNormalStrength,
152
+ ));
153
+ const reflectDir = reflect(V.negate(), particleNormal);
154
+ const sparkle = pow(max(dot(reflectDir, L), 0.0), 1000.0);
155
+ const viewAlignment = pow(max(dot(V, particleNormal), 0.0), 5.0);
156
+ const fade = smoothstep(particleSize.mul(0.2), particleSize, dist).oneMinus();
157
+ const sparkleIntensity = sparkle.mul(viewAlignment).mul(fade).mul(100.0);
158
+ const colorSeed = toonGlitterHash(neighborCell.add(50.0));
159
+ glitter.addAssign(
160
+ mix(vec3(1.0, 0.98, 0.95), vec3(0.95, 0.97, 1.0), colorSeed).mul(sparkleIntensity),
161
+ );
162
+ });
163
+ }
164
+ }
165
+ return glitter;
166
+ };
167
+
168
+ return {
169
+ calculateEyeHighlightMask,
170
+ calculateHairHighlightMask,
171
+ calculateSpecularArea,
172
+ evaluateGlitter,
173
+ };
174
+ }
@@ -0,0 +1,309 @@
1
+ // TSL port of src/shaders/chunks/character-fragment-lighting.glsl — cel
2
+ // banding, face proxy normals, screen-space depth rim + contact shadow, the
3
+ // character-only shadow map, average-shadow blending, and the rim mask.
4
+ //
5
+ // GLSL out-parameters (evaluateDepthEffects) become returned node pairs.
6
+ // Uniform-driven early returns become select()/If() with identical operand
7
+ // math. Screen-space reads use three's screenUV node, which resolves the
8
+ // per-backend viewport orientation the GLSL handled implicitly through
9
+ // gl_FragCoord.
10
+
11
+ import {
12
+ abs,
13
+ clamp,
14
+ dFdx,
15
+ dFdy,
16
+ dot,
17
+ float,
18
+ Fn,
19
+ If,
20
+ length,
21
+ max,
22
+ min,
23
+ mix,
24
+ normalize,
25
+ perspectiveDepthToViewZ,
26
+ pow,
27
+ screenUV,
28
+ select,
29
+ smoothstep,
30
+ step,
31
+ vec2,
32
+ vec3,
33
+ vec4,
34
+ } from 'three/tsl';
35
+
36
+ export function createLightingChunk({ u, tex, v, flags, frontFacingNode, cameraViewMatrixNode }) {
37
+ // Widens a smoothstep transition by the value's screen derivative so the
38
+ // edge stays ~1px feathered. edgeAntiAliasStrength = 0 → plain smoothstep.
39
+ const toonEdgeSmooth = (value, edgeMin, edgeMax) => {
40
+ const derivative = min(
41
+ abs(dFdx(value)).add(abs(dFdy(value))).mul(u.edgeAntiAliasStrength),
42
+ 0.25,
43
+ );
44
+ return smoothstep(edgeMin.sub(derivative), edgeMax.add(derivative), value);
45
+ };
46
+
47
+ // NOTE (docs/tsl-conventions.md): select() operands must stay pure
48
+ // expressions — branches that create vars or call Fn()s crash the GLSL
49
+ // builder's detached type-resolution flow (ConditionalNode fallback setup).
50
+ const resolveLightingNormal = (normal) => {
51
+ const flattenWS = normalize(u.headForwardWS);
52
+ const sphereWS = normalize(v.vWorldPosition.sub(u.headPositionWS));
53
+ const fixedWS = normalize(mix(flattenWS, sphereWS, clamp(u.faceSphereBlend, 0.0, 1.0)));
54
+ const headTracked = normalize(cameraViewMatrixNode.mul(vec4(fixedWS, 0.0)).xyz);
55
+
56
+ const staticProxy = normalize(v.vFaceProxyNormal);
57
+ // faceHeadSpaceMode 1 uses head-bone data (inert until headDataReady).
58
+ const proxyNormal = select(u.faceHeadSpaceMode.equal(1), headTracked, staticProxy);
59
+ const blended = normalize(mix(normal, proxyNormal, clamp(u.faceNormalProxyBlend, 0.0, 1.0)));
60
+
61
+ const headModeUnready = u.faceHeadSpaceMode.equal(1).and(u.headDataReady.not());
62
+ const inactive = u.isFace.not()
63
+ .or(u.faceNormalProxyBlend.lessThanEqual(0.0))
64
+ .or(headModeUnready);
65
+ return select(inactive, normal, blended);
66
+ };
67
+
68
+ const calcCelShade = (N, L) => {
69
+ const NoL = dot(N, L);
70
+ const bodyCel = mix(
71
+ toonEdgeSmooth(NoL, u.celShadeMidPoint.sub(u.celShadeSoftness), u.celShadeMidPoint.add(u.celShadeSoftness)),
72
+ 1.0,
73
+ u.mainLightIgnoreCelShade,
74
+ );
75
+ const faceCel = mix(
76
+ toonEdgeSmooth(
77
+ NoL,
78
+ u.celShadeMidPointForFaceArea.sub(u.celShadeSoftnessForFaceArea),
79
+ u.celShadeMidPointForFaceArea.add(u.celShadeSoftnessForFaceArea),
80
+ ),
81
+ 1.0,
82
+ u.mainLightIgnoreCelShadeForFaceArea,
83
+ );
84
+ return select(u.isFace, faceCel, bodyCel);
85
+ };
86
+
87
+ const localLightBand = (normal, lightDirection) => {
88
+ const halfLambert = dot(normal, lightDirection).mul(0.5).add(0.5);
89
+ const softBand = smoothstep(0.08, 0.92, halfLambert);
90
+ const faceBand = smoothstep(-0.15, 0.85, halfLambert);
91
+ return select(u.isFace, mix(faceBand, 1.0, u.faceLocalLightLift), softBand);
92
+ };
93
+
94
+ const safeNormalizeDir2 = (value) => value.div(max(length(value), 1e-5));
95
+
96
+ // Render-target reads sample at explicit level 0: the RTs have no mips and
97
+ // WGSL forbids implicit-derivative sampling inside non-uniform control flow.
98
+ // Pass targets are float COLOR textures (vec4 on both builders), so .x is
99
+ // the depth channel — depth-texture sampling types differently per builder.
100
+ const readSceneLinearDepth = (screenUv) => {
101
+ const depthSample = tex.sceneDepthTexture.sample(clamp(screenUv, vec2(0.0), vec2(1.0))).level(0).x;
102
+ return perspectiveDepthToViewZ(depthSample, u.cameraNearPlane, u.cameraFarPlane).negate();
103
+ };
104
+
105
+ // depthRim < 0 signals "depth data unavailable" (fresnel rim fallback);
106
+ // contactShadow is 1 when unshadowed.
107
+ const evaluateDepthEffects = (N, L, NoL, NoV) => {
108
+ const depthRim = float(-1.0).toVar();
109
+ const contactShadow = float(1.0).toVar();
110
+
111
+ const allowed = flags.isOutlinePass
112
+ ? null
113
+ : u.sceneDepthReady.and(u.ditherOpacity.greaterThanEqual(1.0));
114
+
115
+ if (allowed !== null) {
116
+ If(allowed, () => {
117
+ const selfDepth = v.vViewPos.z.negate().toVar();
118
+ const screenUv = screenUV.toVar();
119
+
120
+ const safeDistance = max(u.rimDepthSafeDistance, 0.05);
121
+ const closeCurve = float(2.0).div(safeDistance)
122
+ .sub(selfDepth.div(safeDistance.mul(safeDistance)));
123
+ const distanceFix = select(
124
+ selfDepth.greaterThan(safeDistance),
125
+ float(1.0).div(selfDepth),
126
+ mix(float(1.0).div(selfDepth), closeCurve, u.rimDepthCloseWidthReduce),
127
+ ).toVar();
128
+
129
+ const fovFix = abs(u.cameraProjection11).mul(0.0054);
130
+ const aspectFix = vec2(u.sceneDepthResolution.y.div(u.sceneDepthResolution.x), 1.0);
131
+ const offsetScale = aspectFix.mul(fovFix.mul(distanceFix)).mul(0.707).toVar();
132
+ offsetScale.y.mulAssign(smoothstep(1.0, 0.95, screenUv.y));
133
+
134
+ const faceWidthFix = select(u.isFace, float(0.66666), float(1.0));
135
+
136
+ // ---- Rim light ----
137
+ const rimDir = safeNormalizeDir2(L.xy);
138
+ const rimUvOffset = offsetScale.mul(rimDir).mul(u.rimDepthWidth.mul(faceWidthFix)).toVar();
139
+ const rimThreshold = clamp(u.rimDepthThresholdOffset.add(0.05), 0.0, 1.0);
140
+ const selfDepthForRim = selfDepth.add(rimThreshold).toVar();
141
+ const rimFade = float(10.0).div(max(u.rimDepthFadeRange, 1e-3)).toVar();
142
+
143
+ const rim = clamp(
144
+ readSceneLinearDepth(screenUv.add(rimUvOffset)).sub(selfDepthForRim).mul(rimFade),
145
+ 0.0,
146
+ 1.0,
147
+ ).toVar();
148
+
149
+ If(u.rimDepthDottedLineFix, () => {
150
+ const extend = offsetScale.mul(u.rimDepthWidth).mul(0.5);
151
+ const rimA = clamp(readSceneLinearDepth(screenUv.add(rimUvOffset).add(vec2(-1.0, 0.0).mul(extend))).sub(selfDepthForRim).mul(rimFade), 0.0, 1.0);
152
+ const rimB = clamp(readSceneLinearDepth(screenUv.add(rimUvOffset).add(vec2(1.0, 0.0).mul(extend))).sub(selfDepthForRim).mul(rimFade), 0.0, 1.0);
153
+ const rimC = clamp(readSceneLinearDepth(screenUv.add(rimUvOffset).add(vec2(0.0, 1.0).mul(extend))).sub(selfDepthForRim).mul(rimFade), 0.0, 1.0);
154
+ rim.assign(min(min(rim, rimA), min(rimB, rimC)));
155
+ });
156
+
157
+ If(u.rimDepthMask3D, () => {
158
+ const blur = 0.025;
159
+ rim.mulAssign(smoothstep(0.075 - blur, 0.075 + blur, clamp(NoL, 0.0, 1.0).mul(NoV.oneMinus())));
160
+ });
161
+
162
+ rim.mulAssign(smoothstep(u.rimDepthFadeEndDistance, u.rimDepthFadeStartDistance, selfDepth));
163
+ depthRim.assign(rim);
164
+
165
+ // ---- Contact shadow ----
166
+ If(u.contactShadowStrength.greaterThan(0.0), () => {
167
+ const shadowDirVS = vec3(L).toVar();
168
+ If(u.isFace.and(u.contactShadowFaceHeadUpBlend.greaterThan(0.0)).and(u.headDataReady), () => {
169
+ const headUpVS = normalize(cameraViewMatrixNode.mul(vec4(u.headUpWS, 0.0)).xyz);
170
+ shadowDirVS.assign(normalize(mix(L, headUpVS, u.contactShadowFaceHeadUpBlend)));
171
+ });
172
+ const shadowUvOffset = offsetScale
173
+ .mul(safeNormalizeDir2(shadowDirVS.xy))
174
+ .mul(u.contactShadowWidth.mul(faceWidthFix));
175
+ const shadowThreshold = select(u.isFace, float(0.01), float(0.03)).add(u.contactShadowThresholdOffset);
176
+ const shadowFade = float(50.0).div(max(u.contactShadowFadeRange, 1e-3));
177
+ const unshadowed = clamp(
178
+ readSceneLinearDepth(screenUv.add(shadowUvOffset)).sub(selfDepth.sub(shadowThreshold)).mul(shadowFade),
179
+ 0.0,
180
+ 1.0,
181
+ );
182
+ contactShadow.assign(mix(1.0, unshadowed, u.contactShadowStrength));
183
+ });
184
+ });
185
+ }
186
+
187
+ return { contactShadow, depthRim };
188
+ };
189
+
190
+ // ---- Character-only shadow map ----
191
+
192
+ const charSelfShadowCompare = (coord) => {
193
+ return step(coord.z, tex.charSelfShadowMap.sample(coord.xy).level(0).x);
194
+ };
195
+
196
+ const evaluateCharSelfShadowMap = Fn(() => {
197
+ const shadow = float(1.0).toVar();
198
+
199
+ If(u.charSelfShadowReady, () => {
200
+ const worldN = normalize(v.vWorldNormal).mul(select(frontFacingNode, 1.0, -1.0)).toVar();
201
+
202
+ const slopeBias = float(1.1).sub(dot(worldN, u.charSelfShadowLightDirection));
203
+ const samplePosition = v.vWorldPosition
204
+ .add(worldN.mul(slopeBias).mul(u.charSelfShadowNormalBias))
205
+ .add(u.charSelfShadowLightDirection.mul(slopeBias).mul(u.charSelfShadowDepthBias));
206
+
207
+ // Orthographic shadow camera: w is 1, so clip position is already NDC.
208
+ const shadowNdc = u.charSelfShadowMatrix.mul(vec4(samplePosition, 1.0));
209
+ const coord = shadowNdc.xyz.mul(0.5).add(0.5).toVar();
210
+
211
+ const outside = coord.x.lessThan(0.0)
212
+ .or(coord.x.greaterThan(1.0))
213
+ .or(coord.y.lessThan(0.0))
214
+ .or(coord.y.greaterThan(1.0))
215
+ .or(coord.z.greaterThan(1.0))
216
+ .or(coord.z.lessThan(0.0));
217
+
218
+ If(outside.not(), () => {
219
+ const sampled = float(0.0).toVar();
220
+ If(u.charSelfShadowQuality.greaterThanEqual(2.0), () => {
221
+ // 9-tap box PCF (unrolled).
222
+ for (let x = -1; x <= 1; x += 1) {
223
+ for (let y = -1; y <= 1; y += 1) {
224
+ sampled.addAssign(charSelfShadowCompare(
225
+ coord.add(vec3(vec2(x, y).mul(u.charSelfShadowTexelSize), 0.0)),
226
+ ));
227
+ }
228
+ }
229
+ sampled.divAssign(9.0);
230
+ }).ElseIf(u.charSelfShadowQuality.greaterThanEqual(1.0), () => {
231
+ const halfTexel = u.charSelfShadowTexelSize.mul(0.5);
232
+ const total = charSelfShadowCompare(coord.add(vec3(halfTexel.negate(), halfTexel.negate(), 0.0)))
233
+ .add(charSelfShadowCompare(coord.add(vec3(halfTexel, halfTexel.negate(), 0.0))))
234
+ .add(charSelfShadowCompare(coord.add(vec3(halfTexel.negate(), halfTexel, 0.0))))
235
+ .add(charSelfShadowCompare(coord.add(vec3(halfTexel, halfTexel, 0.0))));
236
+ sampled.assign(total.mul(0.25));
237
+ }).Else(() => {
238
+ sampled.assign(charSelfShadowCompare(coord));
239
+ });
240
+
241
+ If(u.charSelfShadowSharpen.greaterThan(0.0), () => {
242
+ sampled.assign(smoothstep(
243
+ float(0.5).sub(u.charSelfShadowSharpen),
244
+ float(0.5).add(u.charSelfShadowSharpen),
245
+ sampled,
246
+ ));
247
+ });
248
+
249
+ const selfDepth = v.vViewPos.z.negate();
250
+ sampled.assign(mix(sampled, 1.0, clamp(selfDepth.sub(u.charSelfShadowFadeDistance.sub(1.0)), 0.0, 1.0)));
251
+
252
+ If(u.charSelfShadowNdotLFix, () => {
253
+ sampled.assign(mix(
254
+ 1.0,
255
+ sampled,
256
+ smoothstep(0.05, 0.2, clamp(dot(worldN, u.charSelfShadowLightDirection), 0.0, 1.0)),
257
+ ));
258
+ });
259
+
260
+ shadow.assign(sampled);
261
+ });
262
+ });
263
+
264
+ return shadow;
265
+ });
266
+
267
+ const getCharacterSelfShadowVisibility = (sceneShadowVisibility) => {
268
+ // The shadow-map evaluation is hoisted into an If-assigned var: an Fn call
269
+ // inside a select() operand breaks the GLSL builder (see NOTE above), and
270
+ // this also skips the map taps entirely unless sourceMode is 2 — the same
271
+ // shape as the GLSL if-chain.
272
+ const mapVisibility = float(1.0).toVar();
273
+ If(u.selfShadowSourceMode.equal(2), () => {
274
+ mapVisibility.assign(evaluateCharSelfShadowMap());
275
+ });
276
+ return select(u.selfShadowSourceMode.equal(1), sceneShadowVisibility, mapVisibility);
277
+ };
278
+
279
+ const applyAverageShadowVisibility = (visibility, sceneShadowVisibility, selfShadowVisibility) => {
280
+ const sourceAverage = visibility.add(sceneShadowVisibility).add(selfShadowVisibility).div(3.0);
281
+ const liftedAverage = max(sourceAverage, u.averageShadowMinLight);
282
+ const easedAverage = mix(liftedAverage, smoothstep(0.0, 1.0, liftedAverage), u.averageShadowSoftness);
283
+ const applied = mix(visibility, easedAverage, u.averageShadowStrength);
284
+ return select(u.averageShadowStrength.lessThanEqual(0.0), visibility, applied);
285
+ };
286
+
287
+ const calculateRimMask = (NoV, NoL, finalShadowArea, depthRim) => {
288
+ const rimRaw = NoV.oneMinus().mul(NoL.mul(0.25).add(0.75));
289
+ const fresnelRim = toonEdgeSmooth(rimRaw, u.rimMidPoint.sub(u.rimSoftness), u.rimMidPoint.add(u.rimSoftness));
290
+ const rim = select(
291
+ u.rimLightMode.equal(1).and(depthRim.greaterThanEqual(0.0)),
292
+ depthRim,
293
+ fresnelRim,
294
+ );
295
+ const masked = rim.mul(mix(1.0, finalShadowArea, u.rimBlockByShadow));
296
+ return select(u.useRimLight, masked, 0.0);
297
+ };
298
+
299
+ return {
300
+ applyAverageShadowVisibility,
301
+ calcCelShade,
302
+ calculateRimMask,
303
+ evaluateDepthEffects,
304
+ getCharacterSelfShadowVisibility,
305
+ localLightBand,
306
+ resolveLightingNormal,
307
+ toonEdgeSmooth,
308
+ };
309
+ }
@@ -0,0 +1,146 @@
1
+ // TSL port of src/shaders/chunks/character-fragment-material-maps.glsl.
2
+ //
3
+ // GLSL compiles each optional sampler in via a USE_TOON_* define so unused
4
+ // units are never declared (ANGLE's 16-sampler limit). Here the same
5
+ // conditions become JS guards: when a map is absent the no-map stub result is
6
+ // baked into the graph, so the WGSL/GLSL the builder emits never references
7
+ // the texture. The `has*`/strength uniforms stay runtime-tweakable exactly
8
+ // like the GLSL versions.
9
+ //
10
+ // The factory receives:
11
+ // u — uniform-node map (GLSL uniform names)
12
+ // tex — texture nodes (only the ones the material actually carries)
13
+ // v — varyings ({ vUv, vUv2, vViewPos })
14
+ // flags — compile-time booleans mirroring the GLSL defines
15
+
16
+ import {
17
+ clamp,
18
+ cross,
19
+ dFdx,
20
+ dFdy,
21
+ dot,
22
+ Fn,
23
+ inverseSqrt,
24
+ max,
25
+ mix,
26
+ normalize,
27
+ select,
28
+ vec2,
29
+ vec3,
30
+ } from 'three/tsl';
31
+
32
+ export function createMaterialMapChunk({ u, tex, v, flags }) {
33
+ const sampleMaterialNormalMapColor = () => {
34
+ if (!flags.hasNormalMap) return vec3(0.5, 0.5, 1.0);
35
+ return select(u.hasMaterialNormalMap, tex.materialNormalMap.sample(v.vUv).rgb, vec3(0.5, 0.5, 1.0));
36
+ };
37
+
38
+ // Pure dataflow (no Fn wrapper, no toVar) — see the select() purity note in
39
+ // character-lighting.js.
40
+ const applyMaterialNormalMap = (N) => {
41
+ if (!flags.hasNormalMap) return N;
42
+ const raw = sampleMaterialNormalMapColor().mul(2.0).sub(1.0);
43
+ const scaledXY = raw.xy.mul(u.materialNormalScale.mul(u.materialNormalStrength));
44
+ const mapN = normalize(vec3(scaledXY, raw.z));
45
+
46
+ // cotangentFrame(N, vViewPos.xyz, vUv), written out inline.
47
+ const p = v.vViewPos.xyz;
48
+ const dp1 = dFdx(p);
49
+ const dp2 = dFdy(p);
50
+ const duv1 = dFdx(v.vUv);
51
+ const duv2 = dFdy(v.vUv);
52
+ const dp2perp = cross(dp2, N);
53
+ const dp1perp = cross(N, dp1);
54
+ const T = dp2perp.mul(duv1.x).add(dp1perp.mul(duv2.x));
55
+ const B = dp2perp.mul(duv1.y).add(dp1perp.mul(duv2.y));
56
+ const invMax = inverseSqrt(max(max(dot(T, T), dot(B, B)), 1.0e-6));
57
+ // mat3(T*invMax, B*invMax, N) * mapN, expanded column-wise.
58
+ const perturbed = normalize(
59
+ T.mul(invMax).mul(mapN.x)
60
+ .add(B.mul(invMax).mul(mapN.y))
61
+ .add(N.mul(mapN.z)),
62
+ );
63
+ const blended = normalize(mix(N, perturbed, clamp(u.materialNormalStrength, 0.0, 1.0)));
64
+ return select(u.useMaterialNormalMap, blended, N);
65
+ };
66
+
67
+ const sampleMaterialAo = () => {
68
+ if (!flags.hasAoMap) return null; // caller substitutes 1.0
69
+ return select(u.hasMaterialAoMap, clamp(tex.materialAoMap.sample(v.vUv2).r, 0.0, 1.0), 1.0);
70
+ };
71
+
72
+ const sampleMaterialDetail = () => {
73
+ if (!flags.hasDetailMap) return vec3(0.5);
74
+ return select(
75
+ u.hasMaterialDetailMap,
76
+ tex.materialDetailMap.sample(v.vUv.mul(u.materialDetailRepeat)).rgb,
77
+ vec3(0.5),
78
+ );
79
+ };
80
+
81
+ const applyMaterialDetail = (albedo) => {
82
+ if (!flags.hasDetailMap) return albedo;
83
+ const detail = sampleMaterialDetail().mul(2.0);
84
+ const applied = albedo.mul(mix(vec3(1.0), detail, clamp(u.materialDetailStrength, 0.0, 1.0)));
85
+ const active = u.hasMaterialDetailMap.and(u.materialDetailStrength.greaterThan(0.0));
86
+ return select(active, applied, albedo);
87
+ };
88
+
89
+ const sampleMaterialEmissive = () => {
90
+ if (!flags.hasEmissiveMap) return u.materialEmissiveColor;
91
+ const texel = select(u.hasMaterialEmissiveMap, tex.materialEmissiveMap.sample(v.vUv).rgb, vec3(1.0));
92
+ return texel.mul(u.materialEmissiveColor);
93
+ };
94
+
95
+ const sampleMaterialMatcap = (N) => {
96
+ if (!flags.hasMatcapMap) return vec3(0.0);
97
+ const matcapUv = N.xy.mul(0.5).add(0.5);
98
+ return select(u.hasMaterialMatcapMap, tex.materialMatcapMap.sample(vec2(matcapUv)).rgb, vec3(0.0));
99
+ };
100
+
101
+ const sampleMaterialRamp = (shadeArea) => {
102
+ if (!flags.hasRampMap) return vec3(1.0);
103
+ return select(
104
+ u.hasMaterialRampMap,
105
+ tex.materialRampMap.sample(vec2(clamp(shadeArea, 0.0, 1.0), 0.5)).rgb,
106
+ vec3(1.0),
107
+ );
108
+ };
109
+
110
+ const sampleMaterialRoughness = () => {
111
+ if (!flags.hasRoughnessMap) return clamp(u.materialRoughness, 0.0, 1.0);
112
+ return select(
113
+ u.hasMaterialRoughnessMap,
114
+ clamp(tex.materialRoughnessMap.sample(v.vUv).g, 0.0, 1.0),
115
+ clamp(u.materialRoughness, 0.0, 1.0),
116
+ );
117
+ };
118
+
119
+ const sampleMaterialMetalness = () => {
120
+ if (!flags.hasMetalnessMap) return clamp(u.materialMetalness, 0.0, 1.0);
121
+ return select(
122
+ u.hasMaterialMetalnessMap,
123
+ clamp(tex.materialMetalnessMap.sample(v.vUv).b, 0.0, 1.0),
124
+ clamp(u.materialMetalness, 0.0, 1.0),
125
+ );
126
+ };
127
+
128
+ const sampleMaterialSpecularColor = () => {
129
+ if (!flags.hasSpecularColorMap) return vec3(1.0);
130
+ return select(u.hasMaterialSpecularColorMap, tex.materialSpecularColorMap.sample(v.vUv).rgb, vec3(1.0));
131
+ };
132
+
133
+ return {
134
+ applyMaterialDetail,
135
+ applyMaterialNormalMap,
136
+ sampleMaterialAo,
137
+ sampleMaterialDetail,
138
+ sampleMaterialEmissive,
139
+ sampleMaterialMatcap,
140
+ sampleMaterialMetalness,
141
+ sampleMaterialNormalMapColor,
142
+ sampleMaterialRamp,
143
+ sampleMaterialRoughness,
144
+ sampleMaterialSpecularColor,
145
+ };
146
+ }