@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,170 @@
1
+ import {
2
+ hasAnyToken,
3
+ materialText,
4
+ roleIsFace,
5
+ roleIsHair,
6
+ roleIsSkin,
7
+ roleIsTransparentOverlay,
8
+ } from '../../core/materialRoles.js';
9
+
10
+ export const DEFAULT_ALPHA_SETTINGS = Object.freeze({
11
+ blendCutoff: 0.02,
12
+ costumeCutout: true,
13
+ cutoutCutoff: 0.35,
14
+ // Bayer-matrix screen-door fade (1 = fully visible). Unlike alpha blending
15
+ // it needs no sorting, keeps depth writes, and works with outlines — the
16
+ // standard way to fade a whole character in/out. Runtime helper:
17
+ // setToonDitherOpacity(root, value).
18
+ ditherOpacity: 1,
19
+ enabled: true,
20
+ expressionTokenCutout: true,
21
+ eyeHighlightOrder: 12,
22
+ eyeOrder: 11,
23
+ faceCutout: true,
24
+ hairCutout: true,
25
+ mapTransparentCutout: true,
26
+ overlayDepthWrite: false,
27
+ overlayOrder: 20,
28
+ preserveSourceAlphaTest: true,
29
+ scleraOrder: 10,
30
+ skinCutout: true,
31
+ sortOverlays: true,
32
+ sourceAlphaMapCutout: true,
33
+ sourceTransparentCutout: true,
34
+ transparentOverlayBlend: true,
35
+ transparentOpacityThreshold: 0.999,
36
+ });
37
+
38
+ function firstDefined(source, keys) {
39
+ for (const key of keys) {
40
+ if (source?.[key] !== undefined) return source[key];
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
46
+ const nextValue = Number(value);
47
+ if (!Number.isFinite(nextValue)) return fallback;
48
+ return Math.min(max, Math.max(min, nextValue));
49
+ }
50
+
51
+ function enabledOption(value) {
52
+ if (typeof value === 'string') {
53
+ const normalized = value.trim().toLowerCase();
54
+ return normalized !== 'off' && normalized !== 'none' && normalized !== 'false' && normalized !== '0';
55
+ }
56
+ return value !== false;
57
+ }
58
+
59
+ function normalizeAlphaOptions(options) {
60
+ if (options === false) return { enabled: false };
61
+ if (options === true) return { enabled: true };
62
+ if (typeof options === 'string') return { enabled: enabledOption(options) };
63
+ return options || {};
64
+ }
65
+
66
+ export function createAlphaSettings(options = null) {
67
+ const source = normalizeAlphaOptions(options);
68
+ const enabled = enabledOption(source.enabled);
69
+
70
+ return {
71
+ blendCutoff: enabled
72
+ ? numberOption(firstDefined(source, ['blendCutoff', 'blendAlphaTest', 'blendThreshold']), DEFAULT_ALPHA_SETTINGS.blendCutoff, { min: 0, max: 1 })
73
+ : -1,
74
+ costumeCutout: source.costumeCutout !== undefined ? enabledOption(source.costumeCutout) : DEFAULT_ALPHA_SETTINGS.costumeCutout,
75
+ cutoutCutoff: enabled
76
+ ? numberOption(firstDefined(source, ['cutoutCutoff', 'alphaCutoff', 'alphaTest', 'cutoutThreshold']), DEFAULT_ALPHA_SETTINGS.cutoutCutoff, { min: 0, max: 1 })
77
+ : -1,
78
+ ditherOpacity: numberOption(
79
+ firstDefined(source, ['ditherOpacity', 'ditherFadeout', 'fadeOpacity']),
80
+ DEFAULT_ALPHA_SETTINGS.ditherOpacity,
81
+ { min: 0, max: 1 },
82
+ ),
83
+ enabled,
84
+ expressionTokenCutout: source.expressionTokenCutout !== undefined ? enabledOption(source.expressionTokenCutout) : DEFAULT_ALPHA_SETTINGS.expressionTokenCutout,
85
+ eyeHighlightOrder: numberOption(firstDefined(source, ['eyeHighlightOrder', 'catchlightOrder']), DEFAULT_ALPHA_SETTINGS.eyeHighlightOrder, { min: -100, max: 100 }),
86
+ eyeOrder: numberOption(firstDefined(source, ['eyeOrder', 'irisOrder', 'pupilOrder']), DEFAULT_ALPHA_SETTINGS.eyeOrder, { min: -100, max: 100 }),
87
+ faceCutout: source.faceCutout !== undefined ? enabledOption(source.faceCutout) : DEFAULT_ALPHA_SETTINGS.faceCutout,
88
+ hairCutout: source.hairCutout !== undefined ? enabledOption(source.hairCutout) : DEFAULT_ALPHA_SETTINGS.hairCutout,
89
+ mapTransparentCutout: source.mapTransparentCutout !== undefined ? enabledOption(source.mapTransparentCutout) : DEFAULT_ALPHA_SETTINGS.mapTransparentCutout,
90
+ overlayDepthWrite: source.overlayDepthWrite !== undefined ? enabledOption(source.overlayDepthWrite) : DEFAULT_ALPHA_SETTINGS.overlayDepthWrite,
91
+ overlayOrder: numberOption(firstDefined(source, ['overlayOrder', 'transparentOverlayOrder']), DEFAULT_ALPHA_SETTINGS.overlayOrder, { min: -100, max: 100 }),
92
+ preserveSourceAlphaTest: source.preserveSourceAlphaTest !== undefined ? enabledOption(source.preserveSourceAlphaTest) : DEFAULT_ALPHA_SETTINGS.preserveSourceAlphaTest,
93
+ scleraOrder: numberOption(firstDefined(source, ['scleraOrder', 'eyeWhiteOrder']), DEFAULT_ALPHA_SETTINGS.scleraOrder, { min: -100, max: 100 }),
94
+ skinCutout: source.skinCutout !== undefined ? enabledOption(source.skinCutout) : DEFAULT_ALPHA_SETTINGS.skinCutout,
95
+ sortOverlays: source.sortOverlays !== undefined ? enabledOption(source.sortOverlays) : DEFAULT_ALPHA_SETTINGS.sortOverlays,
96
+ sourceAlphaMapCutout: source.sourceAlphaMapCutout !== undefined ? enabledOption(source.sourceAlphaMapCutout) : DEFAULT_ALPHA_SETTINGS.sourceAlphaMapCutout,
97
+ sourceTransparentCutout: source.sourceTransparentCutout !== undefined ? enabledOption(source.sourceTransparentCutout) : DEFAULT_ALPHA_SETTINGS.sourceTransparentCutout,
98
+ transparentOverlayBlend: source.transparentOverlayBlend !== undefined ? enabledOption(source.transparentOverlayBlend) : DEFAULT_ALPHA_SETTINGS.transparentOverlayBlend,
99
+ transparentOpacityThreshold: numberOption(
100
+ firstDefined(source, ['transparentOpacityThreshold', 'opaqueOpacityThreshold', 'opacityThreshold']),
101
+ DEFAULT_ALPHA_SETTINGS.transparentOpacityThreshold,
102
+ { min: 0, max: 1 },
103
+ ),
104
+ };
105
+ }
106
+
107
+ export function sourceOpacity(mat) {
108
+ return Number.isFinite(mat?.opacity) ? mat.opacity : 1;
109
+ }
110
+
111
+ export function usesAlphaCutout(mat, roleInfo, settings = createAlphaSettings()) {
112
+ if (!settings.enabled || !mat || roleIsTransparentOverlay(roleInfo)) return false;
113
+
114
+ const text = materialText(mat);
115
+ const tokenCutout = settings.expressionTokenCutout &&
116
+ hasAnyToken(text, ['skin', 'costume', 'cloth', 'clothes', 'hair', 'expression']);
117
+
118
+ return (
119
+ (settings.preserveSourceAlphaTest && Number.isFinite(mat.alphaTest) && mat.alphaTest > 0) ||
120
+ (settings.sourceAlphaMapCutout && Boolean(mat.alphaMap)) ||
121
+ (settings.mapTransparentCutout && mat.map?.transparent === true) ||
122
+ (settings.sourceTransparentCutout && mat.transparent === true && sourceOpacity(mat) >= settings.transparentOpacityThreshold) ||
123
+ (settings.skinCutout && roleIsSkin(roleInfo)) ||
124
+ (settings.faceCutout && roleIsFace(roleInfo)) ||
125
+ (settings.hairCutout && roleIsHair(roleInfo)) ||
126
+ (settings.costumeCutout && roleInfo?.role === 'costume') ||
127
+ tokenCutout
128
+ );
129
+ }
130
+
131
+ export function usesAlphaBlend(mat, roleInfo, settings = createAlphaSettings()) {
132
+ if (!settings.enabled || !mat) return false;
133
+ if (roleIsTransparentOverlay(roleInfo)) return settings.transparentOverlayBlend;
134
+ return mat.transparent === true && sourceOpacity(mat) < settings.transparentOpacityThreshold;
135
+ }
136
+
137
+ export function alphaTestForMaterial(mat, roleInfo, settings = createAlphaSettings()) {
138
+ if (usesAlphaBlend(mat, roleInfo, settings)) {
139
+ return Math.max(mat?.alphaTest ?? settings.blendCutoff, settings.blendCutoff);
140
+ }
141
+ if (usesAlphaCutout(mat, roleInfo, settings)) {
142
+ return Math.max(mat?.alphaTest ?? settings.cutoutCutoff, settings.cutoutCutoff);
143
+ }
144
+ return -1.0;
145
+ }
146
+
147
+ export function resolveAlphaForMaterial(settings, mat, roleInfo) {
148
+ const alphaBlend = usesAlphaBlend(mat, roleInfo, settings);
149
+ const alphaCutout = usesAlphaCutout(mat, roleInfo, settings);
150
+
151
+ return {
152
+ alphaBlend,
153
+ alphaCutout,
154
+ alphaTest: alphaTestForMaterial(mat, roleInfo, settings),
155
+ depthWrite: roleIsTransparentOverlay(roleInfo) ? settings.overlayDepthWrite : !alphaBlend,
156
+ opacity: sourceOpacity(mat),
157
+ transparent: mat?.transparent === true && !alphaCutout,
158
+ };
159
+ }
160
+
161
+ export function materialAlphaDrawOrder(settings, mat, roleInfo) {
162
+ if (!settings.sortOverlays) return 0;
163
+
164
+ const text = materialText(mat);
165
+ if (text.includes('白目') || roleInfo?.role === 'sclera') return settings.scleraOrder;
166
+ if (roleInfo?.role === 'eye' || roleInfo?.role === 'iris' || roleInfo?.role === 'pupil') return settings.eyeOrder;
167
+ if (roleInfo?.role === 'eyeHighlight' || roleInfo?.role === 'catchlight') return settings.eyeHighlightOrder;
168
+ if (roleIsTransparentOverlay(roleInfo)) return settings.overlayOrder;
169
+ return 0;
170
+ }
@@ -0,0 +1,181 @@
1
+ export const DEFAULT_AVERAGE_SHADOW_SETTINGS = Object.freeze({
2
+ defaultMinLight: 0.28,
3
+ defaultStrength: 0.28,
4
+ enabled: false,
5
+ // Blends the per-pixel scene shadow toward a single measured per-character
6
+ // average (sampled around the character's bounding sphere by the runtime
7
+ // pass). Stops shadow boundaries from slicing a character in half when they
8
+ // stand at a shadow edge. Requires characterRenderPasses; inert otherwise.
9
+ measuredBlend: 0.65,
10
+ eyeMinLight: 1,
11
+ eyeStrength: 0,
12
+ faceMinLight: 1,
13
+ faceStrength: 0,
14
+ hairMinLight: 0.3,
15
+ hairStrength: 0.22,
16
+ skinMinLight: 0.4,
17
+ skinStrength: 0.18,
18
+ softness: 0.35,
19
+ });
20
+
21
+ function firstDefined(source, keys) {
22
+ for (const key of keys) {
23
+ if (source?.[key] !== undefined) return source[key];
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
29
+ const nextValue = Number(value);
30
+ if (!Number.isFinite(nextValue)) return fallback;
31
+ return Math.min(max, Math.max(min, nextValue));
32
+ }
33
+
34
+ function normalizeAverageShadowOptions(options) {
35
+ if (options === true) return { enabled: true };
36
+ if (typeof options === 'string') {
37
+ const key = options.trim().toLowerCase();
38
+ return {
39
+ enabled: key !== 'off' && key !== 'none' && key !== 'false' && key !== '0',
40
+ };
41
+ }
42
+ return options || {};
43
+ }
44
+
45
+ function hasExplicitSettings(source) {
46
+ return Object.keys(source).some((key) => key !== 'enabled');
47
+ }
48
+
49
+ export function createAverageShadowSettings(options = null) {
50
+ const source = normalizeAverageShadowOptions(options);
51
+ const enabled = source.enabled === true ||
52
+ (source.enabled !== false && hasExplicitSettings(source));
53
+
54
+ return {
55
+ defaultMinLight: enabled
56
+ ? numberOption(
57
+ firstDefined(source, ['defaultMinLight', 'minLight', 'averageShadowMinLight']),
58
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.defaultMinLight,
59
+ { min: 0, max: 1 },
60
+ )
61
+ : 0,
62
+ defaultStrength: enabled
63
+ ? numberOption(
64
+ firstDefined(source, ['defaultStrength', 'strength', 'averageShadowStrength']),
65
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.defaultStrength,
66
+ { min: 0, max: 1 },
67
+ )
68
+ : 0,
69
+ enabled,
70
+ // measuredBlend is intentionally independent of `enabled`: the smoothing
71
+ // layer and the measured replacement solve different problems.
72
+ measuredBlend: numberOption(
73
+ firstDefined(source, ['measuredBlend', 'measuredShadowBlend', 'averageMeasuredBlend']),
74
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.measuredBlend,
75
+ { min: 0, max: 1 },
76
+ ),
77
+ eyeMinLight: enabled
78
+ ? numberOption(
79
+ firstDefined(source, ['eyeMinLight', 'eyeAverageShadowMinLight']),
80
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.eyeMinLight,
81
+ { min: 0, max: 1 },
82
+ )
83
+ : 0,
84
+ eyeStrength: enabled
85
+ ? numberOption(
86
+ firstDefined(source, ['eyeStrength', 'eyeAverageShadowStrength']),
87
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.eyeStrength,
88
+ { min: 0, max: 1 },
89
+ )
90
+ : 0,
91
+ faceMinLight: enabled
92
+ ? numberOption(
93
+ firstDefined(source, ['faceMinLight', 'faceAverageShadowMinLight']),
94
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.faceMinLight,
95
+ { min: 0, max: 1 },
96
+ )
97
+ : 0,
98
+ faceStrength: enabled
99
+ ? numberOption(
100
+ firstDefined(source, ['faceStrength', 'faceAverageShadowStrength']),
101
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.faceStrength,
102
+ { min: 0, max: 1 },
103
+ )
104
+ : 0,
105
+ hairMinLight: enabled
106
+ ? numberOption(
107
+ firstDefined(source, ['hairMinLight', 'hairAverageShadowMinLight']),
108
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.hairMinLight,
109
+ { min: 0, max: 1 },
110
+ )
111
+ : 0,
112
+ hairStrength: enabled
113
+ ? numberOption(
114
+ firstDefined(source, ['hairStrength', 'hairAverageShadowStrength']),
115
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.hairStrength,
116
+ { min: 0, max: 1 },
117
+ )
118
+ : 0,
119
+ skinMinLight: enabled
120
+ ? numberOption(
121
+ firstDefined(source, ['skinMinLight', 'skinAverageShadowMinLight']),
122
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.skinMinLight,
123
+ { min: 0, max: 1 },
124
+ )
125
+ : 0,
126
+ skinStrength: enabled
127
+ ? numberOption(
128
+ firstDefined(source, ['skinStrength', 'skinAverageShadowStrength']),
129
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.skinStrength,
130
+ { min: 0, max: 1 },
131
+ )
132
+ : 0,
133
+ softness: enabled
134
+ ? numberOption(
135
+ firstDefined(source, ['softness', 'averageShadowSoftness', 'visibilitySoftness']),
136
+ DEFAULT_AVERAGE_SHADOW_SETTINGS.softness,
137
+ { min: 0, max: 1 },
138
+ )
139
+ : 0,
140
+ };
141
+ }
142
+
143
+ export function resolveAverageShadowForMaterial(settings, {
144
+ isEye = false,
145
+ isFace = false,
146
+ isHair = false,
147
+ isSkin = false,
148
+ } = {}) {
149
+ if (isEye) {
150
+ return {
151
+ minLight: settings.eyeMinLight,
152
+ strength: settings.eyeStrength,
153
+ };
154
+ }
155
+
156
+ if (isFace) {
157
+ return {
158
+ minLight: settings.faceMinLight,
159
+ strength: settings.faceStrength,
160
+ };
161
+ }
162
+
163
+ if (isSkin) {
164
+ return {
165
+ minLight: settings.skinMinLight,
166
+ strength: settings.skinStrength,
167
+ };
168
+ }
169
+
170
+ if (isHair) {
171
+ return {
172
+ minLight: settings.hairMinLight,
173
+ strength: settings.hairStrength,
174
+ };
175
+ }
176
+
177
+ return {
178
+ minLight: settings.defaultMinLight,
179
+ strength: settings.defaultStrength,
180
+ };
181
+ }
@@ -0,0 +1,133 @@
1
+ import * as THREE from 'three';
2
+
3
+ export const BASE_TEXTURE_MATERIAL_COLOR_MODES = Object.freeze({
4
+ legacy: 'legacy',
5
+ source: 'source',
6
+ texture: 'texture',
7
+ white: 'white',
8
+ });
9
+
10
+ export const BASE_TEXTURE_SATURATION_MODES = Object.freeze({
11
+ legacy: 'legacy',
12
+ source: 'source',
13
+ custom: 'custom',
14
+ });
15
+
16
+ export const DEFAULT_BASE_TEXTURE_SETTINGS = Object.freeze({
17
+ customSaturation: 1,
18
+ materialColorMode: BASE_TEXTURE_MATERIAL_COLOR_MODES.legacy,
19
+ saturationMode: BASE_TEXTURE_SATURATION_MODES.legacy,
20
+ });
21
+
22
+ const MATERIAL_COLOR_MODE_ALIASES = Object.freeze({
23
+ '': BASE_TEXTURE_MATERIAL_COLOR_MODES.legacy,
24
+ auto: BASE_TEXTURE_MATERIAL_COLOR_MODES.legacy,
25
+ default: BASE_TEXTURE_MATERIAL_COLOR_MODES.legacy,
26
+ ignore: BASE_TEXTURE_MATERIAL_COLOR_MODES.texture,
27
+ legacy: BASE_TEXTURE_MATERIAL_COLOR_MODES.legacy,
28
+ none: BASE_TEXTURE_MATERIAL_COLOR_MODES.white,
29
+ preserve: BASE_TEXTURE_MATERIAL_COLOR_MODES.source,
30
+ raw: BASE_TEXTURE_MATERIAL_COLOR_MODES.source,
31
+ source: BASE_TEXTURE_MATERIAL_COLOR_MODES.source,
32
+ texture: BASE_TEXTURE_MATERIAL_COLOR_MODES.texture,
33
+ textureonly: BASE_TEXTURE_MATERIAL_COLOR_MODES.texture,
34
+ white: BASE_TEXTURE_MATERIAL_COLOR_MODES.white,
35
+ });
36
+
37
+ const SATURATION_MODE_ALIASES = Object.freeze({
38
+ '': BASE_TEXTURE_SATURATION_MODES.legacy,
39
+ auto: BASE_TEXTURE_SATURATION_MODES.legacy,
40
+ custom: BASE_TEXTURE_SATURATION_MODES.custom,
41
+ default: BASE_TEXTURE_SATURATION_MODES.legacy,
42
+ legacy: BASE_TEXTURE_SATURATION_MODES.legacy,
43
+ preserve: BASE_TEXTURE_SATURATION_MODES.source,
44
+ raw: BASE_TEXTURE_SATURATION_MODES.source,
45
+ source: BASE_TEXTURE_SATURATION_MODES.source,
46
+ });
47
+
48
+ function normalizeKey(value) {
49
+ return String(value ?? '')
50
+ .trim()
51
+ .replace(/[\s_-]+/g, '')
52
+ .toLowerCase();
53
+ }
54
+
55
+ function normalizeMaterialColorMode(value, fallback) {
56
+ return MATERIAL_COLOR_MODE_ALIASES[normalizeKey(value)] ?? fallback;
57
+ }
58
+
59
+ function normalizeSaturationMode(value, fallback) {
60
+ return SATURATION_MODE_ALIASES[normalizeKey(value)] ?? fallback;
61
+ }
62
+
63
+ export function createBaseTextureSettings(options = null) {
64
+ if (!options) return DEFAULT_BASE_TEXTURE_SETTINGS;
65
+
66
+ if (typeof options === 'string') {
67
+ return {
68
+ ...DEFAULT_BASE_TEXTURE_SETTINGS,
69
+ materialColorMode: normalizeMaterialColorMode(options, DEFAULT_BASE_TEXTURE_SETTINGS.materialColorMode),
70
+ };
71
+ }
72
+
73
+ const preserveSource = options.preserveSource === true || options.preserve === true;
74
+ const fallbackMode = preserveSource
75
+ ? BASE_TEXTURE_MATERIAL_COLOR_MODES.source
76
+ : DEFAULT_BASE_TEXTURE_SETTINGS.materialColorMode;
77
+ const fallbackSaturationMode = preserveSource
78
+ ? BASE_TEXTURE_SATURATION_MODES.source
79
+ : DEFAULT_BASE_TEXTURE_SETTINGS.saturationMode;
80
+ const customSaturation = Number.isFinite(options.customSaturation)
81
+ ? options.customSaturation
82
+ : Number.isFinite(options.saturation)
83
+ ? options.saturation
84
+ : DEFAULT_BASE_TEXTURE_SETTINGS.customSaturation;
85
+
86
+ return {
87
+ customSaturation,
88
+ materialColorMode: normalizeMaterialColorMode(
89
+ options.materialColorMode ?? options.materialColor ?? options.colorMode,
90
+ fallbackMode,
91
+ ),
92
+ saturationMode: normalizeSaturationMode(
93
+ options.saturationMode ?? options.saturationPolicy,
94
+ fallbackSaturationMode,
95
+ ),
96
+ };
97
+ }
98
+
99
+ export function getSourceMaterialColor(mat) {
100
+ return mat?.color?.isColor ? mat.color : new THREE.Color(1, 1, 1);
101
+ }
102
+
103
+ function isNeutralTextureMultiplier(color) {
104
+ const maxChannel = Math.max(color.r, color.g, color.b);
105
+ const minChannel = Math.min(color.r, color.g, color.b);
106
+ const luma = color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722;
107
+ return maxChannel - minChannel < 0.04 && luma < 0.98;
108
+ }
109
+
110
+ export function resolveBaseMaterialColor(mat, settings = DEFAULT_BASE_TEXTURE_SETTINGS) {
111
+ const sourceColor = getSourceMaterialColor(mat);
112
+ const hasTexture = mat?.map?.isTexture === true;
113
+ const mode = settings.materialColorMode ?? DEFAULT_BASE_TEXTURE_SETTINGS.materialColorMode;
114
+
115
+ if (mode === BASE_TEXTURE_MATERIAL_COLOR_MODES.white) return new THREE.Color(1, 1, 1);
116
+ if (mode === BASE_TEXTURE_MATERIAL_COLOR_MODES.texture && hasTexture) return new THREE.Color(1, 1, 1);
117
+ if (
118
+ mode === BASE_TEXTURE_MATERIAL_COLOR_MODES.legacy &&
119
+ hasTexture &&
120
+ isNeutralTextureMultiplier(sourceColor)
121
+ ) {
122
+ return new THREE.Color(1, 1, 1);
123
+ }
124
+
125
+ return sourceColor;
126
+ }
127
+
128
+ export function resolveBaseMapSaturation({ isFace = false, isSkin = false } = {}, settings = DEFAULT_BASE_TEXTURE_SETTINGS) {
129
+ const mode = settings.saturationMode ?? DEFAULT_BASE_TEXTURE_SETTINGS.saturationMode;
130
+ if (mode === BASE_TEXTURE_SATURATION_MODES.source) return 1;
131
+ if (mode === BASE_TEXTURE_SATURATION_MODES.custom) return settings.customSaturation;
132
+ return isSkin || isFace ? 0.98 : 1.04;
133
+ }
@@ -0,0 +1,96 @@
1
+ export const DEFAULT_CEL_SHADE_SETTINGS = Object.freeze({
2
+ bodyCelMidPoint: 0.06,
3
+ bodyCelSoftness: 0.045,
4
+ bodyMainLightIgnoreCelShade: 0.02,
5
+ edgeAntiAliasStrength: 1,
6
+ enabled: true,
7
+ });
8
+
9
+ export const REFERENCE_CEL_SHADE_SETTINGS = Object.freeze({
10
+ bodyCelMidPoint: 0,
11
+ bodyCelSoftness: 0.05,
12
+ bodyMainLightIgnoreCelShade: 0,
13
+ edgeAntiAliasStrength: 1,
14
+ enabled: true,
15
+ });
16
+
17
+ export const CEL_SHADE_PRESETS = Object.freeze({
18
+ baseline: DEFAULT_CEL_SHADE_SETTINGS,
19
+ reference: REFERENCE_CEL_SHADE_SETTINGS,
20
+ });
21
+
22
+ function firstDefined(source, keys) {
23
+ for (const key of keys) {
24
+ if (source?.[key] !== undefined) return source[key];
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
30
+ const nextValue = Number(value);
31
+ if (!Number.isFinite(nextValue)) return fallback;
32
+ return Math.min(max, Math.max(min, nextValue));
33
+ }
34
+
35
+ function resolvePresetDefaults(source) {
36
+ const presetName = String(firstDefined(source, ['preset', 'profile']) || 'baseline').toLowerCase();
37
+ return CEL_SHADE_PRESETS[presetName] || DEFAULT_CEL_SHADE_SETTINGS;
38
+ }
39
+
40
+ export function createCelShadeSettings(options = null) {
41
+ const source = typeof options === 'string' ? { preset: options } : options || {};
42
+ const presetDefaults = resolvePresetDefaults(source);
43
+ const enabled = source.enabled !== false;
44
+
45
+ return {
46
+ bodyCelMidPoint: enabled
47
+ ? numberOption(
48
+ firstDefined(source, [
49
+ 'bodyCelMidPoint',
50
+ 'bodyCelThreshold',
51
+ 'celMidPoint',
52
+ 'mainCelMidPoint',
53
+ 'mainLightCelMidPoint',
54
+ ]),
55
+ presetDefaults.bodyCelMidPoint,
56
+ { min: -1, max: 1 },
57
+ )
58
+ : presetDefaults.bodyCelMidPoint,
59
+ bodyCelSoftness: enabled
60
+ ? numberOption(
61
+ firstDefined(source, [
62
+ 'bodyCelSoftness',
63
+ 'bodyCelFeather',
64
+ 'celSoftness',
65
+ 'mainCelSoftness',
66
+ 'mainLightCelSoftness',
67
+ ]),
68
+ presetDefaults.bodyCelSoftness,
69
+ { min: 0.001, max: 1 },
70
+ )
71
+ : presetDefaults.bodyCelSoftness,
72
+ bodyMainLightIgnoreCelShade: enabled
73
+ ? numberOption(
74
+ firstDefined(source, [
75
+ 'bodyMainLightIgnoreCelShade',
76
+ 'bodyLightIgnoreCelShade',
77
+ 'mainLightIgnoreCelShade',
78
+ 'lightIgnoreCelShade',
79
+ ]),
80
+ presetDefaults.bodyMainLightIgnoreCelShade,
81
+ { min: 0, max: 1 },
82
+ )
83
+ : presetDefaults.bodyMainLightIgnoreCelShade,
84
+ // Widens cel/rim/specular transitions by the per-pixel screen derivative so
85
+ // band edges stay ~1px feathered at any resolution or camera distance.
86
+ // 0 disables and reproduces the raw smoothstep edge.
87
+ edgeAntiAliasStrength: enabled
88
+ ? numberOption(
89
+ firstDefined(source, ['edgeAntiAliasStrength', 'edgeAntiAlias', 'edgeAA']),
90
+ presetDefaults.edgeAntiAliasStrength ?? DEFAULT_CEL_SHADE_SETTINGS.edgeAntiAliasStrength,
91
+ { min: 0, max: 4 },
92
+ )
93
+ : 0,
94
+ enabled,
95
+ };
96
+ }
@@ -0,0 +1,114 @@
1
+ // Screen-space depth-sampled contact shadow.
2
+ //
3
+ // Samples the scene depth prepass a short screen-space step toward the light;
4
+ // when a closer surface sits just in front of the current pixel along that
5
+ // direction, the pixel is treated as being in that surface's contact shadow.
6
+ // The visible result is the thin anime "hair shadow" line where hair meets the
7
+ // forehead, and grounding shadows where an arm crosses the torso.
8
+ //
9
+ // This only activates while a scene depth prepass is running (see
10
+ // characterRenderPasses.js) and is automatically suppressed during dither
11
+ // fadeout, because a fading character no longer writes reliable depth.
12
+
13
+ export const DEFAULT_CONTACT_SHADOW_SETTINGS = Object.freeze({
14
+ enabled: true,
15
+ // How strongly the contact shadow darkens the cel band. 0 disables.
16
+ strength: 0.5,
17
+ // Face uses the head-up direction blend so hair shadows drape downward even
18
+ // when the light comes from the side. 0 = always use the light direction.
19
+ faceHeadUpBlend: 0,
20
+ // Face receives a separate (usually lower) strength; hair-on-forehead is the
21
+ // main use case, so it defaults on but softer than the body.
22
+ faceStrength: 0.4,
23
+ fadeRange: 1,
24
+ thresholdOffset: 0,
25
+ // Screen-space sample distance, relative to the shared depth-effect width.
26
+ width: 1,
27
+ });
28
+
29
+ function firstDefined(source, keys) {
30
+ for (const key of keys) {
31
+ if (source?.[key] !== undefined) return source[key];
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
37
+ const nextValue = Number(value);
38
+ if (!Number.isFinite(nextValue)) return fallback;
39
+ return Math.min(max, Math.max(min, nextValue));
40
+ }
41
+
42
+ function normalizeContactShadowOptions(options) {
43
+ if (options === false) return { enabled: false };
44
+ if (options === true) return { enabled: true };
45
+ if (typeof options === 'string') {
46
+ const key = options.trim().toLowerCase();
47
+ return { enabled: key !== 'off' && key !== 'none' && key !== 'false' && key !== '0' };
48
+ }
49
+ return options || {};
50
+ }
51
+
52
+ export function createContactShadowSettings(options = null) {
53
+ const source = normalizeContactShadowOptions(options);
54
+ const enabled = source.enabled !== false;
55
+
56
+ return {
57
+ enabled,
58
+ faceHeadUpBlend: numberOption(
59
+ firstDefined(source, ['faceHeadUpBlend', 'faceFixedDirection']),
60
+ DEFAULT_CONTACT_SHADOW_SETTINGS.faceHeadUpBlend,
61
+ { min: 0, max: 1 },
62
+ ),
63
+ faceStrength: enabled
64
+ ? numberOption(
65
+ firstDefined(source, ['faceStrength', 'faceContactShadowStrength']),
66
+ DEFAULT_CONTACT_SHADOW_SETTINGS.faceStrength,
67
+ { min: 0, max: 1 },
68
+ )
69
+ : 0,
70
+ fadeRange: numberOption(
71
+ firstDefined(source, ['fadeRange', 'softness']),
72
+ DEFAULT_CONTACT_SHADOW_SETTINGS.fadeRange,
73
+ { min: 0.01, max: 20 },
74
+ ),
75
+ strength: enabled
76
+ ? numberOption(
77
+ firstDefined(source, ['strength', 'intensity', 'usage']),
78
+ DEFAULT_CONTACT_SHADOW_SETTINGS.strength,
79
+ { min: 0, max: 1 },
80
+ )
81
+ : 0,
82
+ thresholdOffset: numberOption(
83
+ firstDefined(source, ['thresholdOffset', 'depthThresholdOffset']),
84
+ DEFAULT_CONTACT_SHADOW_SETTINGS.thresholdOffset,
85
+ { min: -0.05, max: 1 },
86
+ ),
87
+ width: numberOption(
88
+ firstDefined(source, ['width', 'contactShadowWidth']),
89
+ DEFAULT_CONTACT_SHADOW_SETTINGS.width,
90
+ { min: 0, max: 8 },
91
+ ),
92
+ };
93
+ }
94
+
95
+ export function resolveContactShadowForMaterial(settings, {
96
+ isEye = false,
97
+ isFace = false,
98
+ isOutline = false,
99
+ } = {}) {
100
+ // Eyes are drawn slightly proud of the face surface, so contact shadow
101
+ // reads as dirt on them; keep them clean like the eye scene-shadow default.
102
+ let strength = settings.strength;
103
+ if (isFace) strength = settings.faceStrength;
104
+ if (isEye || isOutline) strength = 0;
105
+
106
+ return {
107
+ enabled: settings.enabled && strength > 0,
108
+ faceHeadUpBlend: settings.faceHeadUpBlend,
109
+ fadeRange: settings.fadeRange,
110
+ strength,
111
+ thresholdOffset: settings.thresholdOffset,
112
+ width: settings.width,
113
+ };
114
+ }