@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,187 @@
1
+ import * as THREE from 'three';
2
+
3
+ export const DEFAULT_EYE_HIGHLIGHT_SETTINGS = Object.freeze({
4
+ color: [1, 1, 1],
5
+ enabled: true,
6
+ intensity: 0.58,
7
+ maskChannel: 0,
8
+ maskMap: null,
9
+ maskStrength: 1,
10
+ power: 22,
11
+ showInShadowArea: 0.4,
12
+ sourceMaskMode: 'off',
13
+ });
14
+
15
+ export const EYE_HIGHLIGHT_MASK_CHANNELS = Object.freeze({
16
+ r: 0,
17
+ red: 0,
18
+ x: 0,
19
+ g: 1,
20
+ green: 1,
21
+ y: 1,
22
+ b: 2,
23
+ blue: 2,
24
+ z: 2,
25
+ a: 3,
26
+ alpha: 3,
27
+ w: 3,
28
+ });
29
+
30
+ export const EYE_HIGHLIGHT_SOURCE_MASK_MODES = Object.freeze({
31
+ off: 'off',
32
+ source: 'source',
33
+ });
34
+
35
+ function firstDefined(source, keys) {
36
+ for (const key of keys) {
37
+ if (source?.[key] !== undefined) return source[key];
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
43
+ const nextValue = Number(value);
44
+ if (!Number.isFinite(nextValue)) return fallback;
45
+ return Math.min(max, Math.max(min, nextValue));
46
+ }
47
+
48
+ function colorOption(value, fallback = DEFAULT_EYE_HIGHLIGHT_SETTINGS.color) {
49
+ if (value?.isColor) return value.clone();
50
+
51
+ if (Array.isArray(value) && value.length >= 3) {
52
+ return new THREE.Color(
53
+ numberOption(value[0], fallback[0] ?? fallback.r ?? 1),
54
+ numberOption(value[1], fallback[1] ?? fallback.g ?? 1),
55
+ numberOption(value[2], fallback[2] ?? fallback.b ?? 1),
56
+ );
57
+ }
58
+
59
+ if (typeof value === 'object' && value !== null) {
60
+ return new THREE.Color(
61
+ numberOption(value.r ?? value.x, fallback[0] ?? fallback.r ?? 1),
62
+ numberOption(value.g ?? value.y, fallback[1] ?? fallback.g ?? 1),
63
+ numberOption(value.b ?? value.z, fallback[2] ?? fallback.b ?? 1),
64
+ );
65
+ }
66
+
67
+ if (typeof value === 'string' || typeof value === 'number') {
68
+ try {
69
+ return new THREE.Color(value);
70
+ } catch {
71
+ // Fall through to fallback.
72
+ }
73
+ }
74
+
75
+ return Array.isArray(fallback)
76
+ ? new THREE.Color(fallback[0], fallback[1], fallback[2])
77
+ : fallback.clone();
78
+ }
79
+
80
+ function enabledOption(value) {
81
+ if (typeof value === 'string') {
82
+ const normalized = value.trim().toLowerCase();
83
+ return normalized !== 'off' && normalized !== 'none' && normalized !== 'false' && normalized !== '0';
84
+ }
85
+ return value !== false;
86
+ }
87
+
88
+ function normalizeEyeHighlightOptions(options) {
89
+ if (options === false) return { enabled: false };
90
+ if (options === true) return { enabled: true };
91
+ if (typeof options === 'string') return { enabled: enabledOption(options) };
92
+ return options || {};
93
+ }
94
+
95
+ function normalizeMaskChannel(value, fallback = DEFAULT_EYE_HIGHLIGHT_SETTINGS.maskChannel) {
96
+ if (Number.isFinite(value)) return Math.min(3, Math.max(0, Math.round(value)));
97
+ const normalized = String(value ?? '').trim().toLowerCase();
98
+ return EYE_HIGHLIGHT_MASK_CHANNELS[normalized] ?? fallback;
99
+ }
100
+
101
+ function normalizeSourceMaskMode(value, fallback = DEFAULT_EYE_HIGHLIGHT_SETTINGS.sourceMaskMode) {
102
+ if (value === true) return EYE_HIGHLIGHT_SOURCE_MASK_MODES.source;
103
+ if (value === false) return EYE_HIGHLIGHT_SOURCE_MASK_MODES.off;
104
+ const normalized = String(value ?? fallback).trim().toLowerCase();
105
+ if (normalized === 'material' || normalized === 'map' || normalized === 'source') {
106
+ return EYE_HIGHLIGHT_SOURCE_MASK_MODES.source;
107
+ }
108
+ return EYE_HIGHLIGHT_SOURCE_MASK_MODES.off;
109
+ }
110
+
111
+ export function createEyeHighlightSettings(options = null) {
112
+ const source = normalizeEyeHighlightOptions(options);
113
+ const enabled = enabledOption(source.enabled);
114
+
115
+ return {
116
+ color: colorOption(
117
+ firstDefined(source, ['color', 'eyeHighlightColor', 'highlightColor', 'glossColor']),
118
+ DEFAULT_EYE_HIGHLIGHT_SETTINGS.color,
119
+ ),
120
+ enabled,
121
+ intensity: enabled
122
+ ? numberOption(
123
+ firstDefined(source, ['intensity', 'eyeHighlightIntensity', 'highlightIntensity', 'glossIntensity']),
124
+ DEFAULT_EYE_HIGHLIGHT_SETTINGS.intensity,
125
+ { min: 0, max: 8 },
126
+ )
127
+ : 0,
128
+ maskChannel: normalizeMaskChannel(firstDefined(source, ['maskChannel', 'eyeHighlightMaskChannel'])),
129
+ maskMap: firstDefined(source, ['maskMap', 'eyeHighlightMaskMap', 'catchlightMaskMap']) ??
130
+ DEFAULT_EYE_HIGHLIGHT_SETTINGS.maskMap,
131
+ maskStrength: enabled
132
+ ? numberOption(
133
+ firstDefined(source, ['maskStrength', 'eyeHighlightMaskStrength', 'catchlightMaskStrength']),
134
+ DEFAULT_EYE_HIGHLIGHT_SETTINGS.maskStrength,
135
+ { min: 0, max: 1 },
136
+ )
137
+ : 0,
138
+ power: enabled
139
+ ? numberOption(
140
+ firstDefined(source, ['power', 'eyeHighlightPower', 'highlightPower', 'glossPower', 'shininess']),
141
+ DEFAULT_EYE_HIGHLIGHT_SETTINGS.power,
142
+ { min: 1, max: 512 },
143
+ )
144
+ : DEFAULT_EYE_HIGHLIGHT_SETTINGS.power,
145
+ showInShadowArea: enabled
146
+ ? numberOption(
147
+ firstDefined(source, [
148
+ 'showInShadowArea',
149
+ 'eyeHighlightShowInShadowArea',
150
+ 'shadowBlend',
151
+ 'shadowLift',
152
+ 'shadowIndependence',
153
+ ]),
154
+ DEFAULT_EYE_HIGHLIGHT_SETTINGS.showInShadowArea,
155
+ { min: 0, max: 1 },
156
+ )
157
+ : 0,
158
+ sourceMaskMode: normalizeSourceMaskMode(
159
+ firstDefined(source, [
160
+ 'sourceMaskMode',
161
+ 'sourceMask',
162
+ 'useSourceMask',
163
+ 'useSourceEyeHighlightMask',
164
+ 'useSourceCatchlightMask',
165
+ ]),
166
+ ),
167
+ };
168
+ }
169
+
170
+ export function resolveEyeHighlightForMaterial(settings, {
171
+ isEye = false,
172
+ isOutline = false,
173
+ maskMap = null,
174
+ } = {}) {
175
+ const resolvedMaskMap = settings.maskMap ?? maskMap;
176
+ return {
177
+ color: settings.color,
178
+ enabled: settings.enabled && isEye && !isOutline && settings.intensity > 0,
179
+ intensity: settings.intensity,
180
+ maskChannel: settings.maskChannel,
181
+ maskMap: resolvedMaskMap,
182
+ maskStrength: settings.maskStrength,
183
+ power: settings.power,
184
+ showInShadowArea: settings.showInShadowArea,
185
+ useMask: Boolean(resolvedMaskMap) && settings.maskStrength > 0,
186
+ };
187
+ }
@@ -0,0 +1,135 @@
1
+ export const FACE_HEAD_SPACE_MODES = Object.freeze({
2
+ static: 'static',
3
+ headBone: 'headBone',
4
+ });
5
+
6
+ export const FACE_HEAD_SPACE_MODE_VALUES = Object.freeze({
7
+ [FACE_HEAD_SPACE_MODES.static]: 0,
8
+ [FACE_HEAD_SPACE_MODES.headBone]: 1,
9
+ });
10
+
11
+ export const DEFAULT_FACE_LIGHTING_SETTINGS = Object.freeze({
12
+ enabled: true,
13
+ faceCelMidPoint: -0.48,
14
+ faceCelSoftness: 0.22,
15
+ faceLocalLightLift: 0.22,
16
+ faceMainLightIgnoreCelShade: 0.45,
17
+ // How strongly the face lighting normal is replaced by the corrected face
18
+ // normal. In headBone mode this only activates when the runtime head tracker
19
+ // provides live head-bone data, so it is inert for plain applyToonShader use.
20
+ faceNormalProxyBlend: 0.75,
21
+ faceProxyNormal: [0, 0, 1],
22
+ faceSceneShadowStrength: 0.5,
23
+ // Blend between the flattened head-forward normal (0) and a proxy sphere
24
+ // around the head bone (1). The mix keeps the lit-to-shadow terminator
25
+ // sweeping smoothly across the face while preserving some roundness.
26
+ faceSphereBlend: 0.75,
27
+ headSpaceMode: FACE_HEAD_SPACE_MODES.headBone,
28
+ });
29
+
30
+ function firstDefined(source, keys) {
31
+ for (const key of keys) {
32
+ if (source?.[key] !== undefined) return source[key];
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
38
+ const nextValue = Number(value);
39
+ if (!Number.isFinite(nextValue)) return fallback;
40
+ return Math.min(max, Math.max(min, nextValue));
41
+ }
42
+
43
+ function vectorOption(value, fallback) {
44
+ if (Array.isArray(value) && value.length >= 3) {
45
+ return [
46
+ numberOption(value[0], fallback[0]),
47
+ numberOption(value[1], fallback[1]),
48
+ numberOption(value[2], fallback[2]),
49
+ ];
50
+ }
51
+
52
+ if (typeof value === 'object' && value !== null) {
53
+ return [
54
+ numberOption(value.x ?? value.r, fallback[0]),
55
+ numberOption(value.y ?? value.g, fallback[1]),
56
+ numberOption(value.z ?? value.b, fallback[2]),
57
+ ];
58
+ }
59
+
60
+ return [...fallback];
61
+ }
62
+
63
+ function headSpaceModeOption(value, fallback) {
64
+ const key = String(value ?? '')
65
+ .trim()
66
+ .replace(/[\s_-]+/g, '')
67
+ .toLowerCase();
68
+ if (['headbone', 'head', 'dynamic', 'tracked'].includes(key)) return FACE_HEAD_SPACE_MODES.headBone;
69
+ if (['static', 'fixed', 'object', 'legacy'].includes(key)) return FACE_HEAD_SPACE_MODES.static;
70
+ return fallback;
71
+ }
72
+
73
+ export function createFaceLightingSettings(options = null) {
74
+ const source = options || {};
75
+ const enabled = source.enabled !== false;
76
+
77
+ return {
78
+ enabled,
79
+ faceCelMidPoint: enabled
80
+ ? numberOption(
81
+ firstDefined(source, ['faceCelMidPoint', 'faceCelThreshold', 'celMidPoint']),
82
+ DEFAULT_FACE_LIGHTING_SETTINGS.faceCelMidPoint,
83
+ { min: -1, max: 1 },
84
+ )
85
+ : DEFAULT_FACE_LIGHTING_SETTINGS.faceCelMidPoint,
86
+ faceCelSoftness: enabled
87
+ ? numberOption(
88
+ firstDefined(source, ['faceCelSoftness', 'faceCelFeather', 'celSoftness']),
89
+ DEFAULT_FACE_LIGHTING_SETTINGS.faceCelSoftness,
90
+ { min: 0.001, max: 1 },
91
+ )
92
+ : DEFAULT_FACE_LIGHTING_SETTINGS.faceCelSoftness,
93
+ faceLocalLightLift: enabled
94
+ ? numberOption(
95
+ firstDefined(source, ['faceLocalLightLift', 'faceLocalLightWrap', 'localLightLift']),
96
+ DEFAULT_FACE_LIGHTING_SETTINGS.faceLocalLightLift,
97
+ { min: 0, max: 1 },
98
+ )
99
+ : DEFAULT_FACE_LIGHTING_SETTINGS.faceLocalLightLift,
100
+ faceMainLightIgnoreCelShade: enabled
101
+ ? numberOption(
102
+ firstDefined(source, ['faceMainLightIgnoreCelShade', 'faceLightIgnoreCelShade', 'mainLightIgnoreCelShade']),
103
+ DEFAULT_FACE_LIGHTING_SETTINGS.faceMainLightIgnoreCelShade,
104
+ { min: 0, max: 1 },
105
+ )
106
+ : DEFAULT_FACE_LIGHTING_SETTINGS.faceMainLightIgnoreCelShade,
107
+ faceNormalProxyBlend: enabled
108
+ ? numberOption(
109
+ firstDefined(source, ['faceNormalProxyBlend', 'faceProxyBlend', 'proxyNormalBlend']),
110
+ DEFAULT_FACE_LIGHTING_SETTINGS.faceNormalProxyBlend,
111
+ { min: 0, max: 1 },
112
+ )
113
+ : 0,
114
+ faceProxyNormal: vectorOption(
115
+ firstDefined(source, ['faceProxyNormal', 'faceNormalProxy', 'proxyNormal']),
116
+ DEFAULT_FACE_LIGHTING_SETTINGS.faceProxyNormal,
117
+ ),
118
+ faceSceneShadowStrength: enabled
119
+ ? numberOption(
120
+ firstDefined(source, ['faceSceneShadowStrength', 'faceReceivedShadowStrength', 'sceneShadowStrength']),
121
+ DEFAULT_FACE_LIGHTING_SETTINGS.faceSceneShadowStrength,
122
+ { min: 0, max: 1 },
123
+ )
124
+ : DEFAULT_FACE_LIGHTING_SETTINGS.faceSceneShadowStrength,
125
+ faceSphereBlend: numberOption(
126
+ firstDefined(source, ['faceSphereBlend', 'sphereBlend', 'proxySphereBlend']),
127
+ DEFAULT_FACE_LIGHTING_SETTINGS.faceSphereBlend,
128
+ { min: 0, max: 1 },
129
+ ),
130
+ headSpaceMode: headSpaceModeOption(
131
+ firstDefined(source, ['headSpaceMode', 'faceShadingMode']),
132
+ DEFAULT_FACE_LIGHTING_SETTINGS.headSpaceMode,
133
+ ),
134
+ };
135
+ }
@@ -0,0 +1,112 @@
1
+ // Shell fur.
2
+ //
3
+ // Renders N progressively-offset copies ("shells") of a mesh; each shell
4
+ // discards fragments by a procedural noise threshold that tightens toward the
5
+ // tip, so the surviving pixels read as fur strands. Fully procedural (no
6
+ // noise texture, no extra samplers), cutout-based (depth-writes, so it sorts
7
+ // correctly against outlines and transparents).
8
+ //
9
+ // Fur is strictly opt-in per material — a fuzzy collar should not make the
10
+ // whole costume shaggy:
11
+ // - fur.materials: array of material name/uuid/regex patterns
12
+ // - fur.roles: array of material roles (e.g. ['costume'])
13
+ // - material.userData.toonFur = true
14
+ //
15
+ // Shells here are mesh clones since WebGL has no geometry shaders.
16
+
17
+ export const DEFAULT_FUR_SETTINGS = Object.freeze({
18
+ enabled: false,
19
+ // Number of shell layers. Cost scales linearly (each shell re-renders the
20
+ // matched mesh), so keep modest; 8-12 reads well at toon scale.
21
+ shellCount: 8,
22
+ // World-space fur length at the outermost shell.
23
+ length: 0.02,
24
+ // 0 = fur sticks straight out; 1 = tips sag fully toward world-down.
25
+ gravity: 0.35,
26
+ // Strand density (noise cells per UV unit, scaled x100 internally).
27
+ density: 3,
28
+ // Shifts the discard threshold toward the roots; more negative = fuller coat.
29
+ rootOffset: -0.2,
30
+ // Darkens fur roots for cheap self-occlusion depth.
31
+ rootShade: 0.55,
32
+ materials: null,
33
+ roles: null,
34
+ });
35
+
36
+ function firstDefined(source, keys) {
37
+ for (const key of keys) {
38
+ if (source?.[key] !== undefined) return source[key];
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
44
+ const nextValue = Number(value);
45
+ if (!Number.isFinite(nextValue)) return fallback;
46
+ return Math.min(max, Math.max(min, nextValue));
47
+ }
48
+
49
+ function patternListOption(value) {
50
+ if (!value) return null;
51
+ const list = Array.isArray(value) ? value : [value];
52
+ const normalized = list.filter((entry) => entry instanceof RegExp || (typeof entry === 'string' && entry.trim() !== ''));
53
+ return normalized.length > 0 ? normalized : null;
54
+ }
55
+
56
+ function roleListOption(value) {
57
+ if (!value) return null;
58
+ const list = (Array.isArray(value) ? value : [value])
59
+ .map((entry) => String(entry ?? '').trim().toLowerCase())
60
+ .filter(Boolean);
61
+ return list.length > 0 ? list : null;
62
+ }
63
+
64
+ function normalizeFurOptions(options) {
65
+ if (options === false) return { enabled: false };
66
+ if (options === true) return { enabled: true };
67
+ return options || {};
68
+ }
69
+
70
+ export function createFurSettings(options = null) {
71
+ const source = normalizeFurOptions(options);
72
+ const enabled = source.enabled === true ||
73
+ (source.enabled !== false && (Boolean(source.materials) || Boolean(source.roles)));
74
+
75
+ return {
76
+ density: numberOption(firstDefined(source, ['density']), DEFAULT_FUR_SETTINGS.density, { min: 0.1, max: 40 }),
77
+ enabled,
78
+ gravity: numberOption(firstDefined(source, ['gravity']), DEFAULT_FUR_SETTINGS.gravity, { min: 0, max: 1 }),
79
+ length: numberOption(firstDefined(source, ['length', 'furLength']), DEFAULT_FUR_SETTINGS.length, { min: 0, max: 1 }),
80
+ materials: patternListOption(firstDefined(source, ['materials', 'materialPatterns'])),
81
+ roles: roleListOption(firstDefined(source, ['roles', 'materialRoles'])),
82
+ rootOffset: numberOption(firstDefined(source, ['rootOffset']), DEFAULT_FUR_SETTINGS.rootOffset, { min: -1, max: 0 }),
83
+ rootShade: numberOption(firstDefined(source, ['rootShade', 'rootDarkening']), DEFAULT_FUR_SETTINGS.rootShade, { min: 0, max: 1 }),
84
+ shellCount: Math.round(numberOption(firstDefined(source, ['shellCount', 'shells', 'layers']), DEFAULT_FUR_SETTINGS.shellCount, { min: 1, max: 32 })),
85
+ };
86
+ }
87
+
88
+ function matchesPattern(patterns, name, uuid) {
89
+ if (!patterns) return false;
90
+ for (const pattern of patterns) {
91
+ if (pattern instanceof RegExp) {
92
+ if (pattern.test(name)) return true;
93
+ } else if (pattern === name || pattern === uuid) {
94
+ return true;
95
+ } else {
96
+ try {
97
+ if (new RegExp(pattern, 'i').test(name)) return true;
98
+ } catch {
99
+ // Invalid pattern strings fall through to exact-match behavior above.
100
+ }
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+
106
+ export function materialUsesFur(settings, sourceMaterial, roleInfo = null) {
107
+ if (!settings?.enabled) return false;
108
+ if (sourceMaterial?.userData?.toonFur === true) return true;
109
+ const role = String(roleInfo?.role ?? '').toLowerCase();
110
+ if (settings.roles?.includes(role)) return true;
111
+ return matchesPattern(settings.materials, sourceMaterial?.name ?? '', sourceMaterial?.uuid ?? '');
112
+ }
@@ -0,0 +1,102 @@
1
+ // Procedural glitter sparkles.
2
+ //
3
+ // Splits UV space into cells, gives each cell one particle with a random
4
+ // orientation, and lights it with a tight reflection lobe — sparkles wink in
5
+ // and out as the view or light moves. Entirely ALU (no textures), but the 3x3
6
+ // neighbor loop is not free, so the whole path compiles out unless enabled.
7
+
8
+ export const DEFAULT_GLITTER_SETTINGS = Object.freeze({
9
+ enabled: false,
10
+ intensity: 1,
11
+ // Cells per UV unit (scaled x1000 internally like typical glitter shaders).
12
+ density: 1,
13
+ size: 1,
14
+ // 0 = particles aligned to the surface, 1 = fully random orientation.
15
+ randomNormalStrength: 0.5,
16
+ // How much glitter survives inside the cel shadow band.
17
+ showInShadowArea: 0.15,
18
+ // UV channel: 0 = uv, 1 = uv2 (unique/non-overlapping UVs sparkle best).
19
+ uvChannel: 1,
20
+ // Role gates: sparkly costumes are the normal use; organic parts stay clean.
21
+ defaultIntensity: 1,
22
+ eyeIntensity: 0,
23
+ faceIntensity: 0,
24
+ hairIntensity: 0,
25
+ skinIntensity: 0,
26
+ });
27
+
28
+ function firstDefined(source, keys) {
29
+ for (const key of keys) {
30
+ if (source?.[key] !== undefined) return source[key];
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
36
+ const nextValue = Number(value);
37
+ if (!Number.isFinite(nextValue)) return fallback;
38
+ return Math.min(max, Math.max(min, nextValue));
39
+ }
40
+
41
+ function normalizeGlitterOptions(options) {
42
+ if (options === false) return { enabled: false };
43
+ if (options === true) return { enabled: true };
44
+ if (typeof options === 'string') {
45
+ const key = options.trim().toLowerCase();
46
+ return { enabled: key !== 'off' && key !== 'none' && key !== 'false' && key !== '0' };
47
+ }
48
+ return options || {};
49
+ }
50
+
51
+ export function createGlitterSettings(options = null) {
52
+ const source = normalizeGlitterOptions(options);
53
+ const enabled = source.enabled === true;
54
+
55
+ return {
56
+ enabled,
57
+ intensity: numberOption(firstDefined(source, ['intensity', 'brightness']), DEFAULT_GLITTER_SETTINGS.intensity, { min: 0, max: 8 }),
58
+ density: numberOption(firstDefined(source, ['density']), DEFAULT_GLITTER_SETTINGS.density, { min: 0.01, max: 8 }),
59
+ size: numberOption(firstDefined(source, ['size', 'particleSize']), DEFAULT_GLITTER_SETTINGS.size, { min: 0.05, max: 4 }),
60
+ randomNormalStrength: numberOption(
61
+ firstDefined(source, ['randomNormalStrength', 'randomness']),
62
+ DEFAULT_GLITTER_SETTINGS.randomNormalStrength,
63
+ { min: 0, max: 1 },
64
+ ),
65
+ showInShadowArea: numberOption(
66
+ firstDefined(source, ['showInShadowArea', 'shadowVisibility']),
67
+ DEFAULT_GLITTER_SETTINGS.showInShadowArea,
68
+ { min: 0, max: 1 },
69
+ ),
70
+ uvChannel: numberOption(firstDefined(source, ['uvChannel']), DEFAULT_GLITTER_SETTINGS.uvChannel, { min: 0, max: 1 }) >= 0.5 ? 1 : 0,
71
+ defaultIntensity: numberOption(firstDefined(source, ['defaultIntensity']), DEFAULT_GLITTER_SETTINGS.defaultIntensity, { min: 0, max: 8 }),
72
+ eyeIntensity: numberOption(firstDefined(source, ['eyeIntensity']), DEFAULT_GLITTER_SETTINGS.eyeIntensity, { min: 0, max: 8 }),
73
+ faceIntensity: numberOption(firstDefined(source, ['faceIntensity']), DEFAULT_GLITTER_SETTINGS.faceIntensity, { min: 0, max: 8 }),
74
+ hairIntensity: numberOption(firstDefined(source, ['hairIntensity']), DEFAULT_GLITTER_SETTINGS.hairIntensity, { min: 0, max: 8 }),
75
+ skinIntensity: numberOption(firstDefined(source, ['skinIntensity']), DEFAULT_GLITTER_SETTINGS.skinIntensity, { min: 0, max: 8 }),
76
+ };
77
+ }
78
+
79
+ export function resolveGlitterForMaterial(settings, {
80
+ isEye = false,
81
+ isFace = false,
82
+ isHair = false,
83
+ isOutline = false,
84
+ isSkin = false,
85
+ } = {}) {
86
+ let roleIntensity = settings.defaultIntensity;
87
+ if (isEye) roleIntensity = settings.eyeIntensity;
88
+ else if (isFace) roleIntensity = settings.faceIntensity;
89
+ else if (isSkin) roleIntensity = settings.skinIntensity;
90
+ else if (isHair) roleIntensity = settings.hairIntensity;
91
+
92
+ const intensity = settings.intensity * roleIntensity;
93
+ return {
94
+ density: settings.density,
95
+ enabled: settings.enabled && !isOutline && intensity > 0,
96
+ intensity,
97
+ randomNormalStrength: settings.randomNormalStrength,
98
+ showInShadowArea: settings.showInShadowArea,
99
+ size: settings.size,
100
+ uvChannel: settings.uvChannel,
101
+ };
102
+ }