@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,325 @@
1
+ export const DEFAULT_HAIR_HIGHLIGHT_SETTINGS = Object.freeze({
2
+ direction: [0, 1, 0.15],
3
+ enabled: true,
4
+ intensity: 0.14,
5
+ maskChannel: 0,
6
+ maskMap: null,
7
+ maskStrength: 1,
8
+ mode: 'legacy',
9
+ shadowFloor: 0.35,
10
+ sideBandPower: 2,
11
+ sourceMaskMode: 'off',
12
+ strandPower: 7,
13
+ uvBandAxis: 0,
14
+ uvBandCenter: 0.5,
15
+ uvBandHalfWidth: 0.5,
16
+ uvPreset: 'center',
17
+ });
18
+
19
+ export const HAIR_HIGHLIGHT_MASK_CHANNELS = Object.freeze({
20
+ r: 0,
21
+ red: 0,
22
+ x: 0,
23
+ g: 1,
24
+ green: 1,
25
+ y: 1,
26
+ b: 2,
27
+ blue: 2,
28
+ z: 2,
29
+ a: 3,
30
+ alpha: 3,
31
+ w: 3,
32
+ });
33
+
34
+ export const HAIR_HIGHLIGHT_MODES = Object.freeze({
35
+ anisotropic: 'anisotropic',
36
+ legacy: 'legacy',
37
+ });
38
+
39
+ export const HAIR_HIGHLIGHT_MODE_VALUES = Object.freeze({
40
+ [HAIR_HIGHLIGHT_MODES.legacy]: 0,
41
+ [HAIR_HIGHLIGHT_MODES.anisotropic]: 1,
42
+ });
43
+
44
+ export const HAIR_HIGHLIGHT_SOURCE_MASK_MODES = Object.freeze({
45
+ off: 'off',
46
+ source: 'source',
47
+ });
48
+
49
+ const UV_PRESETS = Object.freeze({
50
+ center: Object.freeze({ uvBandAxis: 0, uvBandCenter: 0.5, uvBandHalfWidth: 0.5 }),
51
+ full: Object.freeze({ uvBandAxis: 0, uvBandCenter: 0.5, uvBandHalfWidth: 2.0 }),
52
+ left: Object.freeze({ uvBandAxis: 0, uvBandCenter: 0.25, uvBandHalfWidth: 0.35 }),
53
+ right: Object.freeze({ uvBandAxis: 0, uvBandCenter: 0.75, uvBandHalfWidth: 0.35 }),
54
+ vertical: Object.freeze({ uvBandAxis: 1, uvBandCenter: 0.5, uvBandHalfWidth: 0.5 }),
55
+ wide: Object.freeze({ uvBandAxis: 0, uvBandCenter: 0.5, uvBandHalfWidth: 0.75 }),
56
+ });
57
+
58
+ function firstDefined(source, keys) {
59
+ for (const key of keys) {
60
+ if (source?.[key] !== undefined) return source[key];
61
+ }
62
+ return undefined;
63
+ }
64
+
65
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
66
+ const nextValue = Number(value);
67
+ if (!Number.isFinite(nextValue)) return fallback;
68
+ return Math.min(max, Math.max(min, nextValue));
69
+ }
70
+
71
+ function vectorOption(value, fallback) {
72
+ if (Array.isArray(value) && value.length >= 3) {
73
+ return [
74
+ numberOption(value[0], fallback[0]),
75
+ numberOption(value[1], fallback[1]),
76
+ numberOption(value[2], fallback[2]),
77
+ ];
78
+ }
79
+
80
+ if (typeof value === 'object' && value !== null) {
81
+ return [
82
+ numberOption(value.x ?? value.r, fallback[0]),
83
+ numberOption(value.y ?? value.g, fallback[1]),
84
+ numberOption(value.z ?? value.b, fallback[2]),
85
+ ];
86
+ }
87
+
88
+ return [...fallback];
89
+ }
90
+
91
+ function enabledOption(value) {
92
+ if (typeof value === 'string') {
93
+ const normalized = value.trim().toLowerCase();
94
+ return normalized !== 'off' && normalized !== 'none' && normalized !== 'false' && normalized !== '0';
95
+ }
96
+ return value !== false;
97
+ }
98
+
99
+ function normalizeHairHighlightOptions(options) {
100
+ if (options === false) return { enabled: false };
101
+ if (options === true) return { enabled: true };
102
+ if (typeof options === 'string') return { enabled: enabledOption(options) };
103
+ return options || {};
104
+ }
105
+
106
+ function normalizeMaskChannel(value, fallback = DEFAULT_HAIR_HIGHLIGHT_SETTINGS.maskChannel) {
107
+ if (Number.isFinite(value)) return Math.min(3, Math.max(0, Math.round(value)));
108
+ const normalized = String(value ?? '').trim().toLowerCase();
109
+ return HAIR_HIGHLIGHT_MASK_CHANNELS[normalized] ?? fallback;
110
+ }
111
+
112
+ function normalizeMode(value, fallback = DEFAULT_HAIR_HIGHLIGHT_SETTINGS.mode) {
113
+ const normalized = String(value ?? fallback).trim().toLowerCase();
114
+ if (
115
+ normalized === 'aniso' ||
116
+ normalized === 'anisotropic' ||
117
+ normalized === 'kajiyakay' ||
118
+ normalized === 'kajiya-kay' ||
119
+ normalized === 'strand' ||
120
+ normalized === 'strand-highlight' ||
121
+ normalized === 'strand_highlight'
122
+ ) {
123
+ return HAIR_HIGHLIGHT_MODES.anisotropic;
124
+ }
125
+ return HAIR_HIGHLIGHT_MODES.legacy;
126
+ }
127
+
128
+ function normalizeSourceMaskMode(value, fallback = DEFAULT_HAIR_HIGHLIGHT_SETTINGS.sourceMaskMode) {
129
+ if (value === true) return HAIR_HIGHLIGHT_SOURCE_MASK_MODES.source;
130
+ if (value === false) return HAIR_HIGHLIGHT_SOURCE_MASK_MODES.off;
131
+ const normalized = String(value ?? fallback).trim().toLowerCase();
132
+ if (normalized === 'material' || normalized === 'map' || normalized === 'source') {
133
+ return HAIR_HIGHLIGHT_SOURCE_MASK_MODES.source;
134
+ }
135
+ return HAIR_HIGHLIGHT_SOURCE_MASK_MODES.off;
136
+ }
137
+
138
+ function normalizeUvAxis(value, fallback = DEFAULT_HAIR_HIGHLIGHT_SETTINGS.uvBandAxis) {
139
+ if (Number.isFinite(value)) return Math.min(1, Math.max(0, Math.round(value)));
140
+ const normalized = String(value ?? '').trim().toLowerCase();
141
+ if (normalized === 'v' || normalized === 'y' || normalized === 'vertical') return 1;
142
+ if (normalized === 'u' || normalized === 'x' || normalized === 'horizontal') return 0;
143
+ return fallback;
144
+ }
145
+
146
+ function normalizeUvPreset(value, fallback = DEFAULT_HAIR_HIGHLIGHT_SETTINGS.uvPreset) {
147
+ const normalized = String(value ?? fallback).trim().toLowerCase();
148
+ return UV_PRESETS[normalized] ? normalized : fallback;
149
+ }
150
+
151
+ function normalizeMaterialPresets(value) {
152
+ const source = value || {};
153
+ return {
154
+ byName: source.byName || {},
155
+ byUuid: source.byUuid || {},
156
+ patterns: Array.isArray(source.patterns) ? source.patterns : [],
157
+ };
158
+ }
159
+
160
+ function normalizeCoreSettings(source, fallback = DEFAULT_HAIR_HIGHLIGHT_SETTINGS) {
161
+ const enabled = source.enabled === undefined ? fallback.enabled : enabledOption(source.enabled);
162
+ const uvPreset = normalizeUvPreset(firstDefined(source, ['uvPreset', 'preset']), fallback.uvPreset);
163
+ const presetValues = UV_PRESETS[uvPreset] || UV_PRESETS.center;
164
+
165
+ return {
166
+ direction: vectorOption(
167
+ firstDefined(source, ['direction', 'highlightDirection', 'strandDirection']),
168
+ fallback.direction,
169
+ ),
170
+ enabled,
171
+ maskChannel: normalizeMaskChannel(
172
+ firstDefined(source, ['maskChannel', 'hairHighlightMaskChannel']),
173
+ fallback.maskChannel,
174
+ ),
175
+ maskMap: firstDefined(source, ['maskMap', 'hairHighlightMaskMap']) ?? fallback.maskMap,
176
+ maskStrength: enabled
177
+ ? numberOption(
178
+ firstDefined(source, ['maskStrength', 'hairHighlightMaskStrength']),
179
+ fallback.maskStrength,
180
+ { min: 0, max: 1 },
181
+ )
182
+ : 0,
183
+ mode: normalizeMode(firstDefined(source, ['mode', 'highlightMode', 'strandMode']), fallback.mode),
184
+ intensity: enabled
185
+ ? numberOption(
186
+ firstDefined(source, ['intensity', 'hairIntensity', 'hairHighlightIntensity']),
187
+ fallback.intensity,
188
+ { min: 0, max: 8 },
189
+ )
190
+ : 0,
191
+ shadowFloor: enabled
192
+ ? numberOption(
193
+ firstDefined(source, ['shadowFloor', 'shadowLift', 'minimumShadowLight']),
194
+ fallback.shadowFloor,
195
+ { min: 0, max: 1 },
196
+ )
197
+ : fallback.shadowFloor,
198
+ sideBandPower: enabled
199
+ ? numberOption(
200
+ firstDefined(source, ['sideBandPower', 'uvBandPower', 'bandPower']),
201
+ fallback.sideBandPower,
202
+ { min: 0.001, max: 128 },
203
+ )
204
+ : fallback.sideBandPower,
205
+ sourceMaskMode: normalizeSourceMaskMode(
206
+ firstDefined(source, ['sourceMaskMode', 'sourceMask', 'useSourceMask', 'useSourceHairHighlightMask']),
207
+ fallback.sourceMaskMode,
208
+ ),
209
+ strandPower: enabled
210
+ ? numberOption(
211
+ firstDefined(source, ['strandPower', 'power', 'highlightPower']),
212
+ fallback.strandPower,
213
+ { min: 0.001, max: 512 },
214
+ )
215
+ : fallback.strandPower,
216
+ uvBandAxis: normalizeUvAxis(
217
+ firstDefined(source, ['uvBandAxis', 'uvAxis', 'bandAxis']),
218
+ presetValues.uvBandAxis ?? fallback.uvBandAxis,
219
+ ),
220
+ uvBandCenter: enabled
221
+ ? numberOption(
222
+ firstDefined(source, ['uvBandCenter', 'bandCenter']),
223
+ presetValues.uvBandCenter ?? fallback.uvBandCenter,
224
+ { min: -2, max: 3 },
225
+ )
226
+ : presetValues.uvBandCenter ?? fallback.uvBandCenter,
227
+ uvBandHalfWidth: enabled
228
+ ? numberOption(
229
+ firstDefined(source, ['uvBandHalfWidth', 'uvBandWidth', 'bandHalfWidth', 'bandWidth']),
230
+ presetValues.uvBandHalfWidth ?? fallback.uvBandHalfWidth,
231
+ { min: 0.001, max: 4 },
232
+ )
233
+ : presetValues.uvBandHalfWidth ?? fallback.uvBandHalfWidth,
234
+ uvPreset,
235
+ };
236
+ }
237
+
238
+ function optionForMaterial(settings, material) {
239
+ if (!material) return null;
240
+ const materialName = material.name ?? material.sourceMaterialName ?? '';
241
+ const materialUuid = material.uuid ?? material.sourceMaterialUuid ?? '';
242
+ const text = [
243
+ materialName,
244
+ materialUuid,
245
+ material.userData?.sourceMaterialName,
246
+ material.userData?.materialRole,
247
+ material.userData?.materialRoleLabel,
248
+ ].filter(Boolean).join(' ');
249
+ const presets = settings.materialPresets;
250
+ let option = null;
251
+
252
+ for (const entry of presets.patterns) {
253
+ const pattern = entry?.pattern;
254
+ if (!pattern) continue;
255
+ if (
256
+ (pattern instanceof RegExp && pattern.test(text)) ||
257
+ (typeof pattern === 'string' && text.toLowerCase().includes(pattern.toLowerCase()))
258
+ ) {
259
+ option = { ...option, ...entry };
260
+ }
261
+ }
262
+
263
+ if (materialName && presets.byName[materialName]) option = { ...option, ...presets.byName[materialName] };
264
+ if (materialUuid && presets.byUuid[materialUuid]) option = { ...option, ...presets.byUuid[materialUuid] };
265
+
266
+ const userDataOption = material.userData?.toonHairHighlight ??
267
+ material.userData?.hairHighlight ??
268
+ material.userData?.hairHighlightSettings;
269
+ if (userDataOption) option = { ...option, ...userDataOption };
270
+
271
+ return option;
272
+ }
273
+
274
+ function sourceMaskMapForMaterial(material) {
275
+ return material?.userData?.toonHairHighlightMaskMap ??
276
+ material?.userData?.hairHighlightMaskMap ??
277
+ material?.userData?.highlightMaskMap ??
278
+ null;
279
+ }
280
+
281
+ export function createHairHighlightSettings(options = null) {
282
+ const source = normalizeHairHighlightOptions(options);
283
+ return {
284
+ ...normalizeCoreSettings(source),
285
+ materialPresets: normalizeMaterialPresets(
286
+ firstDefined(source, ['materialPresets', 'perMaterial', 'overrides']),
287
+ ),
288
+ };
289
+ }
290
+
291
+ export function resolveHairHighlightForMaterial(settings, {
292
+ isHair = false,
293
+ isOutline = false,
294
+ maskMap = null,
295
+ material = null,
296
+ } = {}) {
297
+ const materialOption = optionForMaterial(settings, material);
298
+ const resolvedSettings = materialOption
299
+ ? normalizeCoreSettings(materialOption, settings)
300
+ : settings;
301
+ const materialSourceMask = resolvedSettings.sourceMaskMode === HAIR_HIGHLIGHT_SOURCE_MASK_MODES.source
302
+ ? sourceMaskMapForMaterial(material)
303
+ : null;
304
+ const resolvedMaskMap = resolvedSettings.maskMap ?? maskMap ?? materialSourceMask;
305
+
306
+ return {
307
+ direction: resolvedSettings.direction,
308
+ enabled: resolvedSettings.enabled && isHair && !isOutline && resolvedSettings.intensity > 0,
309
+ intensity: resolvedSettings.intensity,
310
+ maskChannel: resolvedSettings.maskChannel,
311
+ maskMap: resolvedMaskMap,
312
+ maskStrength: resolvedSettings.maskStrength,
313
+ mode: resolvedSettings.mode,
314
+ modeValue: HAIR_HIGHLIGHT_MODE_VALUES[resolvedSettings.mode] ?? HAIR_HIGHLIGHT_MODE_VALUES.legacy,
315
+ shadowFloor: resolvedSettings.shadowFloor,
316
+ sideBandPower: resolvedSettings.sideBandPower,
317
+ sourceMaskMode: resolvedSettings.sourceMaskMode,
318
+ strandPower: resolvedSettings.strandPower,
319
+ useMask: Boolean(resolvedMaskMap) && resolvedSettings.maskStrength > 0,
320
+ uvBandAxis: resolvedSettings.uvBandAxis,
321
+ uvBandCenter: resolvedSettings.uvBandCenter,
322
+ uvBandHalfWidth: resolvedSettings.uvBandHalfWidth,
323
+ uvPreset: resolvedSettings.uvPreset,
324
+ };
325
+ }
@@ -0,0 +1,244 @@
1
+ import * as THREE from 'three';
2
+
3
+ export const DEFAULT_INDIRECT_LIGHT_SETTINGS = Object.freeze({
4
+ ambientTint: [0.86, 0.91, 1.0],
5
+ defaultIntensity: 0.35,
6
+ defaultMinimumIndirectLight: 0.35,
7
+ enabled: true,
8
+ environmentIndirectLight: 0.56,
9
+ eyeIntensity: 0.35,
10
+ eyeMinimumIndirectLight: 0.35,
11
+ faceIntensity: 0.35,
12
+ faceMinimumIndirectLight: null,
13
+ hairIntensity: 0.35,
14
+ hairMinimumIndirectLight: 0.35,
15
+ hemisphereLightIntensity: 0.42,
16
+ skinIntensity: 0.35,
17
+ skinMinimumIndirectLight: null,
18
+ });
19
+
20
+ function firstDefined(source, keys) {
21
+ for (const key of keys) {
22
+ if (source?.[key] !== undefined) return source[key];
23
+ }
24
+ return undefined;
25
+ }
26
+
27
+ function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
28
+ const nextValue = Number(value);
29
+ if (!Number.isFinite(nextValue)) return fallback;
30
+ return Math.min(max, Math.max(min, nextValue));
31
+ }
32
+
33
+ function nullableNumberOption(value, fallback, bounds) {
34
+ if (value === null) return null;
35
+ if (value === undefined) return fallback;
36
+ return numberOption(value, fallback, bounds);
37
+ }
38
+
39
+ function colorOption(value, fallback) {
40
+ if (value?.isColor) return value.clone();
41
+
42
+ if (Array.isArray(value) && value.length >= 3) {
43
+ return new THREE.Color(
44
+ numberOption(value[0], fallback[0] ?? fallback.r ?? 1),
45
+ numberOption(value[1], fallback[1] ?? fallback.g ?? 1),
46
+ numberOption(value[2], fallback[2] ?? fallback.b ?? 1),
47
+ );
48
+ }
49
+
50
+ if (typeof value === 'object' && value !== null) {
51
+ return new THREE.Color(
52
+ numberOption(value.r ?? value.x, fallback[0] ?? fallback.r ?? 1),
53
+ numberOption(value.g ?? value.y, fallback[1] ?? fallback.g ?? 1),
54
+ numberOption(value.b ?? value.z, fallback[2] ?? fallback.b ?? 1),
55
+ );
56
+ }
57
+
58
+ if (typeof value === 'string' || typeof value === 'number') {
59
+ try {
60
+ return new THREE.Color(value);
61
+ } catch {
62
+ // Fall through to fallback.
63
+ }
64
+ }
65
+
66
+ return Array.isArray(fallback)
67
+ ? new THREE.Color(fallback[0], fallback[1], fallback[2])
68
+ : fallback.clone();
69
+ }
70
+
71
+ function enabledOption(value) {
72
+ if (typeof value === 'string') {
73
+ const normalized = value.trim().toLowerCase();
74
+ return normalized !== 'off' && normalized !== 'none' && normalized !== 'false' && normalized !== '0';
75
+ }
76
+ return value !== false;
77
+ }
78
+
79
+ function normalizeIndirectLightOptions(options) {
80
+ if (options === false) return { enabled: false };
81
+ if (options === true) return { enabled: true };
82
+ if (typeof options === 'string') return { enabled: enabledOption(options) };
83
+ return options || {};
84
+ }
85
+
86
+ function roleNumber(source, role, property, fallback, aliases = []) {
87
+ const rolePrefix = role ? `${role}${property[0].toUpperCase()}${property.slice(1)}` : property;
88
+ return numberOption(
89
+ firstDefined(source, [rolePrefix, ...aliases]),
90
+ fallback,
91
+ { min: 0, max: property === 'minimumIndirectLight' ? 1 : 8 },
92
+ );
93
+ }
94
+
95
+ function nullableRoleNumber(source, role, property, fallback, aliases = []) {
96
+ const rolePrefix = role ? `${role}${property[0].toUpperCase()}${property.slice(1)}` : property;
97
+ return nullableNumberOption(
98
+ firstDefined(source, [rolePrefix, ...aliases]),
99
+ fallback,
100
+ { min: 0, max: property === 'minimumIndirectLight' ? 1 : 8 },
101
+ );
102
+ }
103
+
104
+ export function createIndirectLightSettings(options = null) {
105
+ const source = normalizeIndirectLightOptions(options);
106
+ const enabled = enabledOption(source.enabled);
107
+ const explicitDefaultMinimum = firstDefined(source, [
108
+ 'defaultMinimumIndirectLight',
109
+ 'minimumIndirectLight',
110
+ 'minIndirectLight',
111
+ 'indirectLightFloor',
112
+ ]) !== undefined;
113
+ const defaultIntensity = enabled
114
+ ? roleNumber(
115
+ source,
116
+ 'default',
117
+ 'intensity',
118
+ DEFAULT_INDIRECT_LIGHT_SETTINGS.defaultIntensity,
119
+ ['intensity', 'indirectLightIntensity'],
120
+ )
121
+ : 0;
122
+ const defaultMinimumIndirectLight = enabled
123
+ ? roleNumber(
124
+ source,
125
+ 'default',
126
+ 'minimumIndirectLight',
127
+ DEFAULT_INDIRECT_LIGHT_SETTINGS.defaultMinimumIndirectLight,
128
+ ['minimumIndirectLight', 'minIndirectLight', 'indirectLightFloor'],
129
+ )
130
+ : 0;
131
+ const skinFaceMinimumFallback = explicitDefaultMinimum ? defaultMinimumIndirectLight : null;
132
+
133
+ return {
134
+ ambientTint: colorOption(
135
+ firstDefined(source, ['ambientTint', 'indirectTint', 'indirectLightTint']),
136
+ DEFAULT_INDIRECT_LIGHT_SETTINGS.ambientTint,
137
+ ),
138
+ defaultIntensity,
139
+ defaultMinimumIndirectLight,
140
+ enabled,
141
+ environmentIndirectLight: enabled
142
+ ? numberOption(
143
+ firstDefined(source, ['environmentIndirectLight', 'environmentLight', 'indirectEnvironmentLight']),
144
+ DEFAULT_INDIRECT_LIGHT_SETTINGS.environmentIndirectLight,
145
+ { min: 0, max: 8 },
146
+ )
147
+ : 0,
148
+ eyeIntensity: enabled
149
+ ? roleNumber(source, 'eye', 'intensity', defaultIntensity)
150
+ : 0,
151
+ eyeMinimumIndirectLight: enabled
152
+ ? nullableRoleNumber(
153
+ source,
154
+ 'eye',
155
+ 'minimumIndirectLight',
156
+ defaultMinimumIndirectLight,
157
+ )
158
+ : 0,
159
+ faceIntensity: enabled
160
+ ? roleNumber(source, 'face', 'intensity', defaultIntensity)
161
+ : 0,
162
+ faceMinimumIndirectLight: enabled
163
+ ? nullableRoleNumber(
164
+ source,
165
+ 'face',
166
+ 'minimumIndirectLight',
167
+ skinFaceMinimumFallback,
168
+ ['faceMinIndirectLight'],
169
+ )
170
+ : 0,
171
+ hairIntensity: enabled
172
+ ? roleNumber(source, 'hair', 'intensity', defaultIntensity)
173
+ : 0,
174
+ hairMinimumIndirectLight: enabled
175
+ ? nullableRoleNumber(
176
+ source,
177
+ 'hair',
178
+ 'minimumIndirectLight',
179
+ defaultMinimumIndirectLight,
180
+ )
181
+ : 0,
182
+ hemisphereLightIntensity: enabled
183
+ ? numberOption(
184
+ firstDefined(source, ['hemisphereLightIntensity', 'hemisphereIntensity', 'hemiIntensity']),
185
+ DEFAULT_INDIRECT_LIGHT_SETTINGS.hemisphereLightIntensity,
186
+ { min: 0, max: 8 },
187
+ )
188
+ : 0,
189
+ skinIntensity: enabled
190
+ ? roleNumber(source, 'skin', 'intensity', defaultIntensity)
191
+ : 0,
192
+ skinMinimumIndirectLight: enabled
193
+ ? nullableRoleNumber(
194
+ source,
195
+ 'skin',
196
+ 'minimumIndirectLight',
197
+ skinFaceMinimumFallback,
198
+ ['skinMinIndirectLight'],
199
+ )
200
+ : 0,
201
+ };
202
+ }
203
+
204
+ export function resolveIndirectLightForMaterial(settings, {
205
+ faceMinimumIndirectLightFallback = DEFAULT_INDIRECT_LIGHT_SETTINGS.defaultMinimumIndirectLight,
206
+ isEye = false,
207
+ isFace = false,
208
+ isHair = false,
209
+ isSkin = false,
210
+ skinMinimumIndirectLightFallback = DEFAULT_INDIRECT_LIGHT_SETTINGS.defaultMinimumIndirectLight,
211
+ } = {}) {
212
+ if (isEye) {
213
+ return {
214
+ intensity: settings.eyeIntensity,
215
+ minimumIndirectLight: settings.eyeMinimumIndirectLight ?? settings.defaultMinimumIndirectLight,
216
+ };
217
+ }
218
+
219
+ if (isFace) {
220
+ return {
221
+ intensity: settings.faceIntensity,
222
+ minimumIndirectLight: settings.faceMinimumIndirectLight ?? faceMinimumIndirectLightFallback,
223
+ };
224
+ }
225
+
226
+ if (isSkin) {
227
+ return {
228
+ intensity: settings.skinIntensity,
229
+ minimumIndirectLight: settings.skinMinimumIndirectLight ?? skinMinimumIndirectLightFallback,
230
+ };
231
+ }
232
+
233
+ if (isHair) {
234
+ return {
235
+ intensity: settings.hairIntensity,
236
+ minimumIndirectLight: settings.hairMinimumIndirectLight ?? settings.defaultMinimumIndirectLight,
237
+ };
238
+ }
239
+
240
+ return {
241
+ intensity: settings.defaultIntensity,
242
+ minimumIndirectLight: settings.defaultMinimumIndirectLight,
243
+ };
244
+ }