@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.
- package/ATTRIBUTION.md +40 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +97 -0
- package/src/character/characterRig.js +353 -0
- package/src/character/freestyleSwimClip.js +541 -0
- package/src/character/index.js +4 -0
- package/src/character/modelLoader.js +433 -0
- package/src/core/materialRoles.js +438 -0
- package/src/core/presetDocuments.js +110 -0
- package/src/core/shaderBackend.js +24 -0
- package/src/debrisgen/debrisFields.js +544 -0
- package/src/debrisgen/debrisGenerator.js +1929 -0
- package/src/debrisgen/debrisPalettes.js +71 -0
- package/src/debrisgen/debrisPhysics.js +198 -0
- package/src/debrisgen/debrisPresets.js +155 -0
- package/src/debrisgen/debrisSettings.js +333 -0
- package/src/debrisgen/debrisTextures.js +380 -0
- package/src/debrisgen/index.js +5 -0
- package/src/debug/fieldValues.js +95 -0
- package/src/debug/index.js +7 -0
- package/src/debug/settingsPanel.js +170 -0
- package/src/environment/environmentAmbientProbe.js +160 -0
- package/src/environment/environmentMaterialAdapter.js +336 -0
- package/src/environment/environmentMaterialClassifier.js +178 -0
- package/src/environment/environmentPlanarReflection.js +201 -0
- package/src/environment/environmentPresets.js +443 -0
- package/src/environment/environmentRigs.js +684 -0
- package/src/environment/environmentSettings.js +504 -0
- package/src/environment/environmentShaderMaterials.js +318 -0
- package/src/environment/environmentSunShadowPass.js +268 -0
- package/src/environment/environmentTextureResolver.js +161 -0
- package/src/environment/environmentTimeOfDay.js +167 -0
- package/src/environment/environmentVertexAo.js +188 -0
- package/src/environment/index.js +8 -0
- package/src/environment/scanAssetStylize.js +103 -0
- package/src/index.js +21 -0
- package/src/loaders/index.js +1 -0
- package/src/post/index.js +2 -0
- package/src/post/postProcessing.js +1087 -0
- package/src/rockgen/export/glbExport.js +177 -0
- package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
- package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
- package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
- package/src/rockgen/index.js +14 -0
- package/src/rockgen/mesh/meshAttributes.js +374 -0
- package/src/rockgen/mesh/meshDocument.js +158 -0
- package/src/rockgen/mesh/surfaceNets.js +329 -0
- package/src/rockgen/noise/cellularNoise3.js +92 -0
- package/src/rockgen/noise/prng.js +62 -0
- package/src/rockgen/noise/simplexNoise3.js +89 -0
- package/src/rockgen/noise/valueNoise3.js +87 -0
- package/src/rockgen/rockDocument.js +255 -0
- package/src/rockgen/rockHelpers.js +8 -0
- package/src/rockgen/rockgenPresets.js +559 -0
- package/src/rockgen/rockgenSettings.js +756 -0
- package/src/rockgen/sdf/fieldCompiler.js +544 -0
- package/src/rockgen/sdf/sculptEdits.js +66 -0
- package/src/rockgen/sdf/sdfModifiers.js +45 -0
- package/src/rockgen/sdf/sdfOps.js +37 -0
- package/src/rockgen/sdf/sdfPrimitives.js +88 -0
- package/src/shaders-tsl/anime.js +963 -0
- package/src/shaders-tsl/chunks/character-color.js +64 -0
- package/src/shaders-tsl/chunks/character-highlights.js +174 -0
- package/src/shaders-tsl/chunks/character-lighting.js +309 -0
- package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
- package/src/shaders-tsl/chunks/character-roles.js +52 -0
- package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
- package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
- package/src/shaders-tsl/chunks/character-skinning.js +140 -0
- package/src/shaders-tsl/chunks/environment-color.js +47 -0
- package/src/shaders-tsl/chunks/environment-debug.js +79 -0
- package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
- package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
- package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
- package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
- package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
- package/src/shaders-tsl/chunks/water-color.js +190 -0
- package/src/shaders-tsl/chunks/water-common.js +124 -0
- package/src/shaders-tsl/chunks/water-foam.js +93 -0
- package/src/shaders-tsl/chunks/water-lighting.js +116 -0
- package/src/shaders-tsl/chunks/water-ripple.js +64 -0
- package/src/shaders-tsl/chunks/water-waves.js +100 -0
- package/src/shaders-tsl/environment-ao-overlay.js +60 -0
- package/src/shaders-tsl/environment.js +680 -0
- package/src/shaders-tsl/flower.js +267 -0
- package/src/shaders-tsl/grass.js +193 -0
- package/src/shaders-tsl/post-composite.js +461 -0
- package/src/shaders-tsl/sky.js +175 -0
- package/src/shaders-tsl/tree-leaf.js +264 -0
- package/src/shaders-tsl/water-breaker.js +423 -0
- package/src/shaders-tsl/water-kelp.js +171 -0
- package/src/shaders-tsl/water-rain.js +107 -0
- package/src/shaders-tsl/water-simulation.js +140 -0
- package/src/shaders-tsl/water-splash.js +298 -0
- package/src/shaders-tsl/water.js +509 -0
- package/src/sky/index.js +2 -0
- package/src/sky/stylizedSky.js +341 -0
- package/src/toon/characterRenderPasses.js +646 -0
- package/src/toon/index.js +4 -0
- package/src/toon/settings/alphaSettings.js +170 -0
- package/src/toon/settings/averageShadowSettings.js +181 -0
- package/src/toon/settings/baseTextureSettings.js +133 -0
- package/src/toon/settings/celShadeSettings.js +96 -0
- package/src/toon/settings/contactShadowSettings.js +114 -0
- package/src/toon/settings/eyeHighlightSettings.js +187 -0
- package/src/toon/settings/faceLightingSettings.js +135 -0
- package/src/toon/settings/furSettings.js +112 -0
- package/src/toon/settings/glitterSettings.js +102 -0
- package/src/toon/settings/hairHighlightSettings.js +325 -0
- package/src/toon/settings/indirectLightSettings.js +244 -0
- package/src/toon/settings/localLightSettings.js +176 -0
- package/src/toon/settings/materialMapSettings.js +299 -0
- package/src/toon/settings/outlineSettings.js +263 -0
- package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
- package/src/toon/settings/rimLightSettings.js +268 -0
- package/src/toon/settings/sceneShadowSettings.js +147 -0
- package/src/toon/settings/selfShadowSettings.js +221 -0
- package/src/toon/settings/shadowColorSettings.js +238 -0
- package/src/toon/settings/skinToneSettings.js +150 -0
- package/src/toon/settings/specularSettings.js +314 -0
- package/src/toon/settings/stickerSettings.js +102 -0
- package/src/toon/toonMaterialAdapter.js +2235 -0
- package/src/toon/toonSettings.js +860 -0
- package/src/vegetation/flowerSpecies.js +260 -0
- package/src/vegetation/index.js +10 -0
- package/src/vegetation/stylizedBush.js +136 -0
- package/src/vegetation/stylizedFlower.js +213 -0
- package/src/vegetation/stylizedFlowers.js +297 -0
- package/src/vegetation/stylizedGrass.js +491 -0
- package/src/vegetation/stylizedTree.js +2449 -0
- package/src/vegetation/stylizedTreeFoliage.js +691 -0
- package/src/vegetation/treeExport.js +300 -0
- package/src/vegetation/treeRecipe.js +647 -0
- package/src/water/index.js +2 -0
- package/src/water/water.js +20 -0
- package/src/water/waterBreakerSystem.js +591 -0
- package/src/water/waterInteraction.js +193 -0
- package/src/water/waterMaterial.js +263 -0
- package/src/water/waterRain.js +136 -0
- package/src/water/waterRippleSimulation.js +227 -0
- package/src/water/waterScenePasses.js +415 -0
- package/src/water/waterSettings.js +1138 -0
- package/src/water/waterSplashSystem.js +251 -0
- package/src/water/waterSurface.js +475 -0
- package/src/water/waterVegetation.js +83 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_SKIN_TONE_SETTINGS = Object.freeze({
|
|
4
|
+
enabled: true,
|
|
5
|
+
faceMaxDirectLight: 100,
|
|
6
|
+
faceMinimumIndirectLight: 0.35,
|
|
7
|
+
faceShadowBrightness: 1.0,
|
|
8
|
+
faceShadowSaturation: 1.0,
|
|
9
|
+
faceShadowTint: [1.0, 0.92, 0.9],
|
|
10
|
+
faceShadowTintStrength: 1.0,
|
|
11
|
+
skinMaxDirectLight: 100,
|
|
12
|
+
skinMinimumIndirectLight: 0.35,
|
|
13
|
+
skinShadowBrightness: 0.92,
|
|
14
|
+
skinShadowSaturation: 1.0,
|
|
15
|
+
skinShadowTint: [1.0, 0.76, 0.74],
|
|
16
|
+
skinShadowTintStrength: 1.0,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function firstDefined(source, keys) {
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
if (source?.[key] !== undefined) return source[key];
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
|
|
27
|
+
const nextValue = Number(value);
|
|
28
|
+
if (!Number.isFinite(nextValue)) return fallback;
|
|
29
|
+
return THREE.MathUtils.clamp(nextValue, min, max);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function colorOption(value, fallback) {
|
|
33
|
+
if (value?.isColor) return value.clone();
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(value) && value.length >= 3) {
|
|
36
|
+
return new THREE.Color(
|
|
37
|
+
numberOption(value[0], fallback[0] ?? fallback.r ?? 1),
|
|
38
|
+
numberOption(value[1], fallback[1] ?? fallback.g ?? 1),
|
|
39
|
+
numberOption(value[2], fallback[2] ?? fallback.b ?? 1),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof value === 'object' && value !== null) {
|
|
44
|
+
const r = numberOption(value.r, fallback[0] ?? fallback.r ?? 1);
|
|
45
|
+
const g = numberOption(value.g, fallback[1] ?? fallback.g ?? 1);
|
|
46
|
+
const b = numberOption(value.b, fallback[2] ?? fallback.b ?? 1);
|
|
47
|
+
return new THREE.Color(r, g, b);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
51
|
+
try {
|
|
52
|
+
return new THREE.Color(value);
|
|
53
|
+
} catch {
|
|
54
|
+
// Fall through to the normalized fallback below.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Array.isArray(fallback)
|
|
59
|
+
? new THREE.Color(fallback[0], fallback[1], fallback[2])
|
|
60
|
+
: fallback.clone();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createSkinToneSettings(options = null) {
|
|
64
|
+
const source = options || {};
|
|
65
|
+
const enabled = source.enabled !== false;
|
|
66
|
+
const skinShadowTintStrength = enabled
|
|
67
|
+
? numberOption(
|
|
68
|
+
firstDefined(source, ['skinShadowTintStrength', 'skinShadowStrength', 'skinTintStrength']),
|
|
69
|
+
DEFAULT_SKIN_TONE_SETTINGS.skinShadowTintStrength,
|
|
70
|
+
{ min: 0, max: 1 },
|
|
71
|
+
)
|
|
72
|
+
: 0;
|
|
73
|
+
const faceShadowTintStrength = enabled
|
|
74
|
+
? numberOption(
|
|
75
|
+
firstDefined(source, ['faceShadowTintStrength', 'faceShadowStrength', 'faceTintStrength']),
|
|
76
|
+
DEFAULT_SKIN_TONE_SETTINGS.faceShadowTintStrength,
|
|
77
|
+
{ min: 0, max: 1 },
|
|
78
|
+
)
|
|
79
|
+
: 0;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
enabled,
|
|
83
|
+
faceMaxDirectLight: enabled
|
|
84
|
+
? numberOption(
|
|
85
|
+
firstDefined(source, ['faceMaxDirectLight', 'faceDirectLightMax', 'maxFaceDirectLight']),
|
|
86
|
+
DEFAULT_SKIN_TONE_SETTINGS.faceMaxDirectLight,
|
|
87
|
+
{ min: 0.05, max: 100 },
|
|
88
|
+
)
|
|
89
|
+
: DEFAULT_SKIN_TONE_SETTINGS.faceMaxDirectLight,
|
|
90
|
+
faceMinimumIndirectLight: enabled
|
|
91
|
+
? numberOption(
|
|
92
|
+
firstDefined(source, ['faceMinimumIndirectLight', 'faceMinIndirectLight', 'faceIndirectLightMin']),
|
|
93
|
+
DEFAULT_SKIN_TONE_SETTINGS.faceMinimumIndirectLight,
|
|
94
|
+
{ min: 0, max: 1 },
|
|
95
|
+
)
|
|
96
|
+
: DEFAULT_SKIN_TONE_SETTINGS.faceMinimumIndirectLight,
|
|
97
|
+
faceShadowBrightness: enabled
|
|
98
|
+
? numberOption(
|
|
99
|
+
firstDefined(source, ['faceShadowBrightness', 'faceShadowValue', 'faceShadowLightness']),
|
|
100
|
+
DEFAULT_SKIN_TONE_SETTINGS.faceShadowBrightness,
|
|
101
|
+
{ min: 0, max: 4 },
|
|
102
|
+
)
|
|
103
|
+
: DEFAULT_SKIN_TONE_SETTINGS.faceShadowBrightness,
|
|
104
|
+
faceShadowSaturation: enabled
|
|
105
|
+
? numberOption(
|
|
106
|
+
firstDefined(source, ['faceShadowSaturation', 'faceShadowSat', 'shadowSaturation']),
|
|
107
|
+
DEFAULT_SKIN_TONE_SETTINGS.faceShadowSaturation,
|
|
108
|
+
{ min: 0, max: 4 },
|
|
109
|
+
)
|
|
110
|
+
: DEFAULT_SKIN_TONE_SETTINGS.faceShadowSaturation,
|
|
111
|
+
faceShadowTint: colorOption(
|
|
112
|
+
firstDefined(source, ['faceShadowTint', 'faceShadowColor', 'faceTint']),
|
|
113
|
+
DEFAULT_SKIN_TONE_SETTINGS.faceShadowTint,
|
|
114
|
+
),
|
|
115
|
+
faceShadowTintStrength,
|
|
116
|
+
skinMaxDirectLight: enabled
|
|
117
|
+
? numberOption(
|
|
118
|
+
firstDefined(source, ['skinMaxDirectLight', 'skinDirectLightMax', 'maxSkinDirectLight']),
|
|
119
|
+
DEFAULT_SKIN_TONE_SETTINGS.skinMaxDirectLight,
|
|
120
|
+
{ min: 0.05, max: 100 },
|
|
121
|
+
)
|
|
122
|
+
: DEFAULT_SKIN_TONE_SETTINGS.skinMaxDirectLight,
|
|
123
|
+
skinMinimumIndirectLight: enabled
|
|
124
|
+
? numberOption(
|
|
125
|
+
firstDefined(source, ['skinMinimumIndirectLight', 'skinMinIndirectLight', 'skinIndirectLightMin']),
|
|
126
|
+
DEFAULT_SKIN_TONE_SETTINGS.skinMinimumIndirectLight,
|
|
127
|
+
{ min: 0, max: 1 },
|
|
128
|
+
)
|
|
129
|
+
: DEFAULT_SKIN_TONE_SETTINGS.skinMinimumIndirectLight,
|
|
130
|
+
skinShadowBrightness: enabled
|
|
131
|
+
? numberOption(
|
|
132
|
+
firstDefined(source, ['skinShadowBrightness', 'skinShadowValue', 'skinShadowLightness']),
|
|
133
|
+
DEFAULT_SKIN_TONE_SETTINGS.skinShadowBrightness,
|
|
134
|
+
{ min: 0, max: 4 },
|
|
135
|
+
)
|
|
136
|
+
: DEFAULT_SKIN_TONE_SETTINGS.skinShadowBrightness,
|
|
137
|
+
skinShadowSaturation: enabled
|
|
138
|
+
? numberOption(
|
|
139
|
+
firstDefined(source, ['skinShadowSaturation', 'skinShadowSat', 'shadowSaturation']),
|
|
140
|
+
DEFAULT_SKIN_TONE_SETTINGS.skinShadowSaturation,
|
|
141
|
+
{ min: 0, max: 4 },
|
|
142
|
+
)
|
|
143
|
+
: DEFAULT_SKIN_TONE_SETTINGS.skinShadowSaturation,
|
|
144
|
+
skinShadowTint: colorOption(
|
|
145
|
+
firstDefined(source, ['skinShadowTint', 'skinShadowColor', 'skinTint']),
|
|
146
|
+
DEFAULT_SKIN_TONE_SETTINGS.skinShadowTint,
|
|
147
|
+
),
|
|
148
|
+
skinShadowTintStrength,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
export const SPECULAR_MASK_CHANNELS = Object.freeze({
|
|
4
|
+
r: 0,
|
|
5
|
+
red: 0,
|
|
6
|
+
x: 0,
|
|
7
|
+
g: 1,
|
|
8
|
+
green: 1,
|
|
9
|
+
y: 1,
|
|
10
|
+
b: 2,
|
|
11
|
+
blue: 2,
|
|
12
|
+
z: 2,
|
|
13
|
+
a: 3,
|
|
14
|
+
alpha: 3,
|
|
15
|
+
w: 3,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const SPECULAR_SOURCE_MASK_MODES = Object.freeze({
|
|
19
|
+
off: 'off',
|
|
20
|
+
source: 'source',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 'light': classic Blinn-style highlight from the half vector (moves with the
|
|
24
|
+
// light). 'view': uses the view angle instead, so the highlight stays
|
|
25
|
+
// anchored to the surface as lights animate — a stable, matcap-like
|
|
26
|
+
// response that suits eyes and metal trim.
|
|
27
|
+
export const SPECULAR_DIRECTION_MODES = Object.freeze({
|
|
28
|
+
light: 'light',
|
|
29
|
+
view: 'view',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const SPECULAR_DIRECTION_MODE_VALUES = Object.freeze({
|
|
33
|
+
[SPECULAR_DIRECTION_MODES.light]: 0,
|
|
34
|
+
[SPECULAR_DIRECTION_MODES.view]: 1,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_SPECULAR_SETTINGS = Object.freeze({
|
|
38
|
+
defaultColor: [1, 0.96, 0.9],
|
|
39
|
+
defaultIntensity: 0.075,
|
|
40
|
+
defaultMidPoint: 0.72,
|
|
41
|
+
defaultPower: 56,
|
|
42
|
+
defaultRange: 0.12,
|
|
43
|
+
defaultShowInShadowArea: 0.25,
|
|
44
|
+
directionMode: SPECULAR_DIRECTION_MODES.light,
|
|
45
|
+
enabled: true,
|
|
46
|
+
eyeIntensity: 0.62,
|
|
47
|
+
eyeMidPoint: 0.35,
|
|
48
|
+
eyePower: 18,
|
|
49
|
+
eyeRange: 0.18,
|
|
50
|
+
eyeShowInShadowArea: 1,
|
|
51
|
+
faceIntensity: 0.025,
|
|
52
|
+
hairIntensity: 0.18,
|
|
53
|
+
hairPower: 40,
|
|
54
|
+
maskChannel: SPECULAR_MASK_CHANNELS.r,
|
|
55
|
+
maskMap: null,
|
|
56
|
+
maskStrength: 1,
|
|
57
|
+
metalIntensity: 0.075,
|
|
58
|
+
skinIntensity: 0.025,
|
|
59
|
+
sourceMaskMode: SPECULAR_SOURCE_MASK_MODES.off,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function firstDefined(source, keys) {
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
if (source?.[key] !== undefined) return source[key];
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function numberOption(value, fallback, { min = -Infinity, max = Infinity } = {}) {
|
|
70
|
+
const nextValue = Number(value);
|
|
71
|
+
if (!Number.isFinite(nextValue)) return fallback;
|
|
72
|
+
return Math.min(max, Math.max(min, nextValue));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function colorOption(value, fallback) {
|
|
76
|
+
if (value?.isColor) return value.clone();
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(value) && value.length >= 3) {
|
|
79
|
+
return new THREE.Color(
|
|
80
|
+
numberOption(value[0], fallback[0] ?? fallback.r ?? 1),
|
|
81
|
+
numberOption(value[1], fallback[1] ?? fallback.g ?? 1),
|
|
82
|
+
numberOption(value[2], fallback[2] ?? fallback.b ?? 1),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof value === 'object' && value !== null) {
|
|
87
|
+
return new THREE.Color(
|
|
88
|
+
numberOption(value.r ?? value.x, fallback[0] ?? fallback.r ?? 1),
|
|
89
|
+
numberOption(value.g ?? value.y, fallback[1] ?? fallback.g ?? 1),
|
|
90
|
+
numberOption(value.b ?? value.z, fallback[2] ?? fallback.b ?? 1),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
95
|
+
try {
|
|
96
|
+
return new THREE.Color(value);
|
|
97
|
+
} catch {
|
|
98
|
+
// Fall through to fallback.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Array.isArray(fallback)
|
|
103
|
+
? new THREE.Color(fallback[0], fallback[1], fallback[2])
|
|
104
|
+
: fallback.clone();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function enabledOption(value) {
|
|
108
|
+
if (typeof value === 'string') {
|
|
109
|
+
const normalized = value.trim().toLowerCase();
|
|
110
|
+
return normalized !== 'off' && normalized !== 'none' && normalized !== 'false' && normalized !== '0';
|
|
111
|
+
}
|
|
112
|
+
return value !== false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeSpecularOptions(options) {
|
|
116
|
+
if (options === false) return { enabled: false };
|
|
117
|
+
if (options === true) return { enabled: true };
|
|
118
|
+
if (typeof options === 'string') return { enabled: enabledOption(options) };
|
|
119
|
+
return options || {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeSourceMaskMode(value) {
|
|
123
|
+
if (value === true) return SPECULAR_SOURCE_MASK_MODES.source;
|
|
124
|
+
if (value === false) return SPECULAR_SOURCE_MASK_MODES.off;
|
|
125
|
+
const normalized = String(value ?? SPECULAR_SOURCE_MASK_MODES.off).trim().toLowerCase();
|
|
126
|
+
if (normalized === 'material' || normalized === 'map' || normalized === 'source') {
|
|
127
|
+
return SPECULAR_SOURCE_MASK_MODES.source;
|
|
128
|
+
}
|
|
129
|
+
return SPECULAR_SOURCE_MASK_MODES.off;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeMaskChannel(value) {
|
|
133
|
+
if (Number.isFinite(value)) return Math.min(3, Math.max(0, Math.round(value)));
|
|
134
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
135
|
+
return SPECULAR_MASK_CHANNELS[normalized] ?? DEFAULT_SPECULAR_SETTINGS.maskChannel;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function roleNumber(source, role, property, fallback, aliases = [], bounds = {}) {
|
|
139
|
+
const rolePrefix = role ? `${role}${property[0].toUpperCase()}${property.slice(1)}` : property;
|
|
140
|
+
return numberOption(firstDefined(source, [rolePrefix, ...aliases]), fallback, bounds);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeDirectionMode(value) {
|
|
144
|
+
const key = String(value ?? '')
|
|
145
|
+
.trim()
|
|
146
|
+
.replace(/[\s_-]+/g, '')
|
|
147
|
+
.toLowerCase();
|
|
148
|
+
if (['view', 'nov', 'stable', 'matcaplike', 'lv', 'lequalsv'].includes(key)) {
|
|
149
|
+
return SPECULAR_DIRECTION_MODES.view;
|
|
150
|
+
}
|
|
151
|
+
return SPECULAR_DIRECTION_MODES.light;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createSpecularSettings(options = null) {
|
|
155
|
+
const source = normalizeSpecularOptions(options);
|
|
156
|
+
const enabled = enabledOption(source.enabled);
|
|
157
|
+
const defaultIntensity = enabled
|
|
158
|
+
? roleNumber(
|
|
159
|
+
source,
|
|
160
|
+
'default',
|
|
161
|
+
'intensity',
|
|
162
|
+
DEFAULT_SPECULAR_SETTINGS.defaultIntensity,
|
|
163
|
+
['intensity', 'specularIntensity'],
|
|
164
|
+
{ min: 0, max: 8 },
|
|
165
|
+
)
|
|
166
|
+
: 0;
|
|
167
|
+
const defaultMidPoint = enabled
|
|
168
|
+
? roleNumber(
|
|
169
|
+
source,
|
|
170
|
+
'default',
|
|
171
|
+
'midPoint',
|
|
172
|
+
DEFAULT_SPECULAR_SETTINGS.defaultMidPoint,
|
|
173
|
+
['midPoint', 'specularMidPoint', 'specularAreaRemapMidPoint'],
|
|
174
|
+
{ min: 0, max: 1 },
|
|
175
|
+
)
|
|
176
|
+
: DEFAULT_SPECULAR_SETTINGS.defaultMidPoint;
|
|
177
|
+
const defaultRange = enabled
|
|
178
|
+
? roleNumber(
|
|
179
|
+
source,
|
|
180
|
+
'default',
|
|
181
|
+
'range',
|
|
182
|
+
DEFAULT_SPECULAR_SETTINGS.defaultRange,
|
|
183
|
+
['range', 'specularRange', 'specularAreaRemapRange'],
|
|
184
|
+
{ min: 0.001, max: 1 },
|
|
185
|
+
)
|
|
186
|
+
: DEFAULT_SPECULAR_SETTINGS.defaultRange;
|
|
187
|
+
const defaultPower = enabled
|
|
188
|
+
? roleNumber(
|
|
189
|
+
source,
|
|
190
|
+
'default',
|
|
191
|
+
'power',
|
|
192
|
+
DEFAULT_SPECULAR_SETTINGS.defaultPower,
|
|
193
|
+
['power', 'specularPower', 'shininess'],
|
|
194
|
+
{ min: 1, max: 512 },
|
|
195
|
+
)
|
|
196
|
+
: DEFAULT_SPECULAR_SETTINGS.defaultPower;
|
|
197
|
+
const defaultShowInShadowArea = enabled
|
|
198
|
+
? roleNumber(
|
|
199
|
+
source,
|
|
200
|
+
'default',
|
|
201
|
+
'showInShadowArea',
|
|
202
|
+
DEFAULT_SPECULAR_SETTINGS.defaultShowInShadowArea,
|
|
203
|
+
['showInShadowArea', 'specularShowInShadowArea', 'shadowVisibility'],
|
|
204
|
+
{ min: 0, max: 1 },
|
|
205
|
+
)
|
|
206
|
+
: 0;
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
defaultColor: colorOption(
|
|
210
|
+
firstDefined(source, ['color', 'specularColor', 'defaultColor']),
|
|
211
|
+
DEFAULT_SPECULAR_SETTINGS.defaultColor,
|
|
212
|
+
),
|
|
213
|
+
defaultIntensity,
|
|
214
|
+
defaultMidPoint,
|
|
215
|
+
defaultPower,
|
|
216
|
+
defaultRange,
|
|
217
|
+
defaultShowInShadowArea,
|
|
218
|
+
directionMode: normalizeDirectionMode(
|
|
219
|
+
firstDefined(source, ['directionMode', 'specularMode', 'highlightMode']),
|
|
220
|
+
),
|
|
221
|
+
enabled,
|
|
222
|
+
eyeIntensity: enabled
|
|
223
|
+
? roleNumber(source, 'eye', 'intensity', DEFAULT_SPECULAR_SETTINGS.eyeIntensity, [], { min: 0, max: 8 })
|
|
224
|
+
: 0,
|
|
225
|
+
eyeMidPoint: enabled
|
|
226
|
+
? roleNumber(source, 'eye', 'midPoint', DEFAULT_SPECULAR_SETTINGS.eyeMidPoint, [], { min: 0, max: 1 })
|
|
227
|
+
: DEFAULT_SPECULAR_SETTINGS.eyeMidPoint,
|
|
228
|
+
eyePower: enabled
|
|
229
|
+
? roleNumber(source, 'eye', 'power', DEFAULT_SPECULAR_SETTINGS.eyePower, [], { min: 1, max: 512 })
|
|
230
|
+
: DEFAULT_SPECULAR_SETTINGS.eyePower,
|
|
231
|
+
eyeRange: enabled
|
|
232
|
+
? roleNumber(source, 'eye', 'range', DEFAULT_SPECULAR_SETTINGS.eyeRange, [], { min: 0.001, max: 1 })
|
|
233
|
+
: DEFAULT_SPECULAR_SETTINGS.eyeRange,
|
|
234
|
+
eyeShowInShadowArea: enabled
|
|
235
|
+
? roleNumber(source, 'eye', 'showInShadowArea', DEFAULT_SPECULAR_SETTINGS.eyeShowInShadowArea, [], { min: 0, max: 1 })
|
|
236
|
+
: 0,
|
|
237
|
+
faceIntensity: enabled
|
|
238
|
+
? roleNumber(source, 'face', 'intensity', DEFAULT_SPECULAR_SETTINGS.faceIntensity, [], { min: 0, max: 8 })
|
|
239
|
+
: 0,
|
|
240
|
+
hairIntensity: enabled
|
|
241
|
+
? roleNumber(source, 'hair', 'intensity', DEFAULT_SPECULAR_SETTINGS.hairIntensity, [], { min: 0, max: 8 })
|
|
242
|
+
: 0,
|
|
243
|
+
hairPower: enabled
|
|
244
|
+
? roleNumber(source, 'hair', 'power', DEFAULT_SPECULAR_SETTINGS.hairPower, [], { min: 1, max: 512 })
|
|
245
|
+
: DEFAULT_SPECULAR_SETTINGS.hairPower,
|
|
246
|
+
maskChannel: normalizeMaskChannel(firstDefined(source, ['maskChannel', 'specularMaskChannel'])),
|
|
247
|
+
maskMap: firstDefined(source, ['maskMap', 'specularMaskMap']) ?? DEFAULT_SPECULAR_SETTINGS.maskMap,
|
|
248
|
+
maskStrength: enabled
|
|
249
|
+
? numberOption(
|
|
250
|
+
firstDefined(source, ['maskStrength', 'specularMaskStrength']),
|
|
251
|
+
DEFAULT_SPECULAR_SETTINGS.maskStrength,
|
|
252
|
+
{ min: 0, max: 1 },
|
|
253
|
+
)
|
|
254
|
+
: 0,
|
|
255
|
+
metalIntensity: enabled
|
|
256
|
+
? roleNumber(source, 'metal', 'intensity', DEFAULT_SPECULAR_SETTINGS.metalIntensity, [], { min: 0, max: 8 })
|
|
257
|
+
: 0,
|
|
258
|
+
skinIntensity: enabled
|
|
259
|
+
? roleNumber(source, 'skin', 'intensity', DEFAULT_SPECULAR_SETTINGS.skinIntensity, [], { min: 0, max: 8 })
|
|
260
|
+
: 0,
|
|
261
|
+
sourceMaskMode: normalizeSourceMaskMode(
|
|
262
|
+
firstDefined(source, ['sourceMaskMode', 'sourceMask', 'useSourceMask', 'useSourceSpecularMap']),
|
|
263
|
+
),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function resolveSpecularForMaterial(settings, {
|
|
268
|
+
isEye = false,
|
|
269
|
+
isFace = false,
|
|
270
|
+
isHair = false,
|
|
271
|
+
isMetal = false,
|
|
272
|
+
isOutline = false,
|
|
273
|
+
isSkin = false,
|
|
274
|
+
maskMap = null,
|
|
275
|
+
} = {}) {
|
|
276
|
+
let intensity = settings.defaultIntensity;
|
|
277
|
+
let midPoint = settings.defaultMidPoint;
|
|
278
|
+
let power = settings.defaultPower;
|
|
279
|
+
let range = settings.defaultRange;
|
|
280
|
+
let showInShadowArea = settings.defaultShowInShadowArea;
|
|
281
|
+
|
|
282
|
+
if (isEye) {
|
|
283
|
+
intensity = settings.eyeIntensity;
|
|
284
|
+
midPoint = settings.eyeMidPoint;
|
|
285
|
+
power = settings.eyePower;
|
|
286
|
+
range = settings.eyeRange;
|
|
287
|
+
showInShadowArea = settings.eyeShowInShadowArea;
|
|
288
|
+
} else if (isFace) {
|
|
289
|
+
intensity = settings.faceIntensity;
|
|
290
|
+
} else if (isSkin) {
|
|
291
|
+
intensity = settings.skinIntensity;
|
|
292
|
+
} else if (isHair) {
|
|
293
|
+
intensity = settings.hairIntensity;
|
|
294
|
+
power = settings.hairPower;
|
|
295
|
+
} else if (isMetal) {
|
|
296
|
+
intensity = settings.metalIntensity;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const resolvedMaskMap = settings.maskMap ?? maskMap;
|
|
300
|
+
return {
|
|
301
|
+
color: settings.defaultColor,
|
|
302
|
+
directionModeValue: SPECULAR_DIRECTION_MODE_VALUES[settings.directionMode] ?? 0,
|
|
303
|
+
enabled: settings.enabled && !isOutline && intensity > 0,
|
|
304
|
+
intensity,
|
|
305
|
+
maskChannel: settings.maskChannel,
|
|
306
|
+
maskMap: resolvedMaskMap,
|
|
307
|
+
maskStrength: settings.maskStrength,
|
|
308
|
+
midPoint,
|
|
309
|
+
power,
|
|
310
|
+
range,
|
|
311
|
+
showInShadowArea,
|
|
312
|
+
useMask: Boolean(resolvedMaskMap) && settings.maskStrength > 0,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Sticker / decal overlay.
|
|
2
|
+
//
|
|
3
|
+
// Blends an overlay texture into the albedo before lighting, so the sticker
|
|
4
|
+
// shades with the same cel bands as the rest of the surface. Covers the
|
|
5
|
+
// "runtime texture overlay" use case (ice, rock, tattoos, damage decals).
|
|
6
|
+
// The sampler is compiled in per material only when a map is present.
|
|
7
|
+
//
|
|
8
|
+
// Map sources, in priority order:
|
|
9
|
+
// 1. settings.map (applies to every matching material)
|
|
10
|
+
// 2. material.userData.toonStickerMap / stickerMap (per-material)
|
|
11
|
+
|
|
12
|
+
export const STICKER_BLEND_MODES = Object.freeze({
|
|
13
|
+
normal: 'normal',
|
|
14
|
+
add: 'add',
|
|
15
|
+
multiply: 'multiply',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const STICKER_BLEND_MODE_VALUES = Object.freeze({
|
|
19
|
+
[STICKER_BLEND_MODES.normal]: 0,
|
|
20
|
+
[STICKER_BLEND_MODES.add]: 1,
|
|
21
|
+
[STICKER_BLEND_MODES.multiply]: 2,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_STICKER_SETTINGS = Object.freeze({
|
|
25
|
+
blendMode: STICKER_BLEND_MODES.normal,
|
|
26
|
+
enabled: false,
|
|
27
|
+
map: null,
|
|
28
|
+
offset: [0, 0],
|
|
29
|
+
repeat: [1, 1],
|
|
30
|
+
strength: 1,
|
|
31
|
+
// 0 = uv, 1 = uv2.
|
|
32
|
+
uvChannel: 0,
|
|
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 vector2Option(value, fallback) {
|
|
49
|
+
if (Array.isArray(value) && value.length >= 2) {
|
|
50
|
+
return [numberOption(value[0], fallback[0]), numberOption(value[1], fallback[1])];
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === 'object' && value !== null) {
|
|
53
|
+
return [numberOption(value.x, fallback[0]), numberOption(value.y, fallback[1])];
|
|
54
|
+
}
|
|
55
|
+
return [...fallback];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function blendModeOption(value, fallback) {
|
|
59
|
+
const key = String(value ?? '').trim().toLowerCase();
|
|
60
|
+
if (['add', 'additive', 'screen'].includes(key)) return STICKER_BLEND_MODES.add;
|
|
61
|
+
if (['multiply', 'mul', 'darken'].includes(key)) return STICKER_BLEND_MODES.multiply;
|
|
62
|
+
if (['normal', 'blend', 'alpha', 'alphablend'].includes(key)) return STICKER_BLEND_MODES.normal;
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeStickerOptions(options) {
|
|
67
|
+
if (options === false) return { enabled: false };
|
|
68
|
+
if (options === true) return { enabled: true };
|
|
69
|
+
return options || {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createStickerSettings(options = null) {
|
|
73
|
+
const source = normalizeStickerOptions(options);
|
|
74
|
+
const enabled = source.enabled === true || (source.enabled !== false && Boolean(source.map));
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
blendMode: blendModeOption(firstDefined(source, ['blendMode', 'blend', 'mode']), DEFAULT_STICKER_SETTINGS.blendMode),
|
|
78
|
+
enabled,
|
|
79
|
+
map: firstDefined(source, ['map', 'stickerMap', 'texture']) ?? DEFAULT_STICKER_SETTINGS.map,
|
|
80
|
+
offset: vector2Option(firstDefined(source, ['offset', 'uvOffset']), DEFAULT_STICKER_SETTINGS.offset),
|
|
81
|
+
repeat: vector2Option(firstDefined(source, ['repeat', 'uvRepeat', 'tiling']), DEFAULT_STICKER_SETTINGS.repeat),
|
|
82
|
+
strength: numberOption(firstDefined(source, ['strength', 'intensity', 'opacity']), DEFAULT_STICKER_SETTINGS.strength, { min: 0, max: 1 }),
|
|
83
|
+
uvChannel: numberOption(firstDefined(source, ['uvChannel']), DEFAULT_STICKER_SETTINGS.uvChannel, { min: 0, max: 1 }) >= 0.5 ? 1 : 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function resolveStickerForMaterial(settings, { isOutline = false, sourceMaterial = null } = {}) {
|
|
88
|
+
const materialMap = sourceMaterial?.userData?.toonStickerMap ??
|
|
89
|
+
sourceMaterial?.userData?.stickerMap ??
|
|
90
|
+
null;
|
|
91
|
+
const map = settings.map ?? materialMap;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
blendModeValue: STICKER_BLEND_MODE_VALUES[settings.blendMode] ?? 0,
|
|
95
|
+
enabled: settings.enabled && !isOutline && Boolean(map) && settings.strength > 0,
|
|
96
|
+
map,
|
|
97
|
+
offset: settings.offset,
|
|
98
|
+
repeat: settings.repeat,
|
|
99
|
+
strength: settings.strength,
|
|
100
|
+
uvChannel: settings.uvChannel,
|
|
101
|
+
};
|
|
102
|
+
}
|