@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,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
|
+
}
|