@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,438 @@
|
|
|
1
|
+
const EMPTY_ROLE_OVERRIDES = Object.freeze({
|
|
2
|
+
byName: new Map(),
|
|
3
|
+
byUuid: new Map(),
|
|
4
|
+
patterns: [],
|
|
5
|
+
sourcesByUuid: new Map(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const MATERIAL_ROLES = Object.freeze({
|
|
9
|
+
default: 0,
|
|
10
|
+
costume: 1,
|
|
11
|
+
skin: 2,
|
|
12
|
+
face: 3,
|
|
13
|
+
hair: 4,
|
|
14
|
+
eye: 5,
|
|
15
|
+
eyeHighlight: 6,
|
|
16
|
+
blush: 7,
|
|
17
|
+
transparentOverlay: 8,
|
|
18
|
+
metal: 9,
|
|
19
|
+
outline: 10,
|
|
20
|
+
iris: 11,
|
|
21
|
+
pupil: 12,
|
|
22
|
+
sclera: 13,
|
|
23
|
+
catchlight: 14,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const MATERIAL_ROLE_LABELS = Object.freeze({
|
|
27
|
+
default: 'Default',
|
|
28
|
+
costume: 'Costume',
|
|
29
|
+
skin: 'Skin',
|
|
30
|
+
face: 'Face',
|
|
31
|
+
hair: 'Hair',
|
|
32
|
+
eye: 'Eye',
|
|
33
|
+
eyeHighlight: 'Eye Highlight',
|
|
34
|
+
blush: 'Blush',
|
|
35
|
+
transparentOverlay: 'Transparent Overlay',
|
|
36
|
+
metal: 'Metal',
|
|
37
|
+
outline: 'Outline',
|
|
38
|
+
iris: 'Iris',
|
|
39
|
+
pupil: 'Pupil',
|
|
40
|
+
sclera: 'Sclera',
|
|
41
|
+
catchlight: 'Catchlight',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const MATERIAL_ROLE_NAMES_BY_VALUE = Object.freeze(
|
|
45
|
+
Object.fromEntries(Object.entries(MATERIAL_ROLES).map(([name, value]) => [value, name])),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const ROLE_ALIASES = Object.freeze({
|
|
49
|
+
'': 'default',
|
|
50
|
+
base: 'default',
|
|
51
|
+
body: 'skin',
|
|
52
|
+
catchlight: 'catchlight',
|
|
53
|
+
cloth: 'costume',
|
|
54
|
+
clothes: 'costume',
|
|
55
|
+
default: 'default',
|
|
56
|
+
decal: 'transparentOverlay',
|
|
57
|
+
dress: 'costume',
|
|
58
|
+
eyehi: 'eyeHighlight',
|
|
59
|
+
eyehighlight: 'eyeHighlight',
|
|
60
|
+
eyecatch: 'catchlight',
|
|
61
|
+
eyecatchlight: 'catchlight',
|
|
62
|
+
eyeglint: 'catchlight',
|
|
63
|
+
eyeshine: 'eyeHighlight',
|
|
64
|
+
highlight: 'eyeHighlight',
|
|
65
|
+
iris: 'iris',
|
|
66
|
+
metallic: 'metal',
|
|
67
|
+
none: 'default',
|
|
68
|
+
outfit: 'costume',
|
|
69
|
+
overlay: 'transparentOverlay',
|
|
70
|
+
pupil: 'pupil',
|
|
71
|
+
sclera: 'sclera',
|
|
72
|
+
skin: 'skin',
|
|
73
|
+
transparent: 'transparentOverlay',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const CATCHLIGHT_TOKENS = ['catchlight', 'eye catch', 'eye_catch', 'eye glint', 'eyeglint'];
|
|
77
|
+
const EYE_HIGHLIGHT_TOKENS = ['目星', 'highlight', 'hi_eye', 'eye_hi', 'sparkle'];
|
|
78
|
+
const BLUSH_TOKENS = ['blush', 'cheek', '脸红', 'face red'];
|
|
79
|
+
const TRANSPARENT_OVERLAY_TOKENS = ['tear', 'decal'];
|
|
80
|
+
const IRIS_TOKENS = ['iris', '虹彩', '虹膜'];
|
|
81
|
+
const PUPIL_TOKENS = ['pupil', '瞳孔'];
|
|
82
|
+
const SCLERA_TOKENS = ['sclera', 'eye white', 'eyewhite', '白目'];
|
|
83
|
+
const EYE_TOKENS = ['eye', 'iris', 'pupil', 'eyeball', '白目', '目'];
|
|
84
|
+
const FACE_TOKENS = [
|
|
85
|
+
'face',
|
|
86
|
+
'head',
|
|
87
|
+
'mouth',
|
|
88
|
+
'tooth',
|
|
89
|
+
'teeth',
|
|
90
|
+
'brow',
|
|
91
|
+
'lash',
|
|
92
|
+
'颜',
|
|
93
|
+
'顏',
|
|
94
|
+
'面',
|
|
95
|
+
'面部',
|
|
96
|
+
'顔',
|
|
97
|
+
'脸',
|
|
98
|
+
'脸部',
|
|
99
|
+
'臉',
|
|
100
|
+
'臉部',
|
|
101
|
+
'表情',
|
|
102
|
+
'睫',
|
|
103
|
+
'二重',
|
|
104
|
+
'眉',
|
|
105
|
+
'嘴',
|
|
106
|
+
'口',
|
|
107
|
+
'口舌',
|
|
108
|
+
'齿',
|
|
109
|
+
'齒',
|
|
110
|
+
'白目',
|
|
111
|
+
];
|
|
112
|
+
const SKIN_TOKENS = ['skin', 'body', 'hand', 'arm', 'leg', '肌', '皮肤', '皮膚', '体', '體', '身体', '身體'];
|
|
113
|
+
const HAIR_TOKENS = ['hair', '髪', '髮', '发', '發', '头发', '頭髮'];
|
|
114
|
+
const METAL_TOKENS = ['metal', 'metallic', 'gold', 'silver', 'bronze', 'steel', '金属', '金屬'];
|
|
115
|
+
const COSTUME_TOKENS = [
|
|
116
|
+
'cloth',
|
|
117
|
+
'clothes',
|
|
118
|
+
'costume',
|
|
119
|
+
'dress',
|
|
120
|
+
'skirt',
|
|
121
|
+
'sleeve',
|
|
122
|
+
'outfit',
|
|
123
|
+
'jacket',
|
|
124
|
+
'coat',
|
|
125
|
+
'服',
|
|
126
|
+
'衣',
|
|
127
|
+
'袖',
|
|
128
|
+
'裙',
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
function normalizeRoleKey(value) {
|
|
132
|
+
return String(value ?? '')
|
|
133
|
+
.trim()
|
|
134
|
+
.replace(/[\s_-]+/g, '')
|
|
135
|
+
.toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function textureLabel(texture) {
|
|
139
|
+
const image = texture?.image;
|
|
140
|
+
return [
|
|
141
|
+
texture?.name,
|
|
142
|
+
texture?.userData?.sourceName,
|
|
143
|
+
texture?.userData?.sourceUri,
|
|
144
|
+
texture?.userData?.gltfImageName,
|
|
145
|
+
texture?.userData?.gltfImageUri,
|
|
146
|
+
image?.currentSrc,
|
|
147
|
+
image?.src,
|
|
148
|
+
image?.name,
|
|
149
|
+
].filter(Boolean).join(' ');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function materialText(mat) {
|
|
153
|
+
const mmd = mat?.userData?.MMD || {};
|
|
154
|
+
const source = mat?.userData?.toonSource || {};
|
|
155
|
+
return [
|
|
156
|
+
mat?.name,
|
|
157
|
+
mmd.name,
|
|
158
|
+
mmd.englishName,
|
|
159
|
+
mmd.comment,
|
|
160
|
+
source.format,
|
|
161
|
+
source.materialName,
|
|
162
|
+
source.textureName,
|
|
163
|
+
source.imageName,
|
|
164
|
+
source.imageUri,
|
|
165
|
+
textureLabel(mat?.map),
|
|
166
|
+
textureLabel(mat?.alphaMap),
|
|
167
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function hasAnyToken(text, tokens) {
|
|
171
|
+
return tokens.some((token) => text.includes(token));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function normalizeMaterialRole(value, fallback = 'default') {
|
|
175
|
+
if (Number.isFinite(value)) {
|
|
176
|
+
return MATERIAL_ROLE_NAMES_BY_VALUE[Math.round(value)] ?? fallback;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const direct = String(value ?? '').trim();
|
|
180
|
+
if (direct in MATERIAL_ROLES) return direct;
|
|
181
|
+
|
|
182
|
+
const key = normalizeRoleKey(value);
|
|
183
|
+
const alias = ROLE_ALIASES[key];
|
|
184
|
+
if (alias && alias in MATERIAL_ROLES) return alias;
|
|
185
|
+
|
|
186
|
+
return fallback;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function materialRoleValue(value) {
|
|
190
|
+
return MATERIAL_ROLES[normalizeMaterialRole(value)] ?? MATERIAL_ROLES.default;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function materialRoleName(value) {
|
|
194
|
+
return normalizeMaterialRole(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function mapEntriesToRoleMap(value) {
|
|
198
|
+
if (!value) return new Map();
|
|
199
|
+
|
|
200
|
+
const entries = value instanceof Map
|
|
201
|
+
? [...value.entries()]
|
|
202
|
+
: Object.entries(value);
|
|
203
|
+
|
|
204
|
+
return new Map(entries
|
|
205
|
+
.filter(([key]) => key !== undefined && key !== null && String(key).trim() !== '')
|
|
206
|
+
.map(([key, role]) => [String(key), normalizeMaterialRole(role)]));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizePattern(pattern) {
|
|
210
|
+
if (pattern instanceof RegExp) return pattern;
|
|
211
|
+
if (typeof pattern !== 'string' || pattern.trim() === '') return null;
|
|
212
|
+
|
|
213
|
+
const trimmed = pattern.trim();
|
|
214
|
+
if (trimmed.startsWith('/') && trimmed.lastIndexOf('/') > 0) {
|
|
215
|
+
const lastSlash = trimmed.lastIndexOf('/');
|
|
216
|
+
const source = trimmed.slice(1, lastSlash);
|
|
217
|
+
const flags = trimmed.slice(lastSlash + 1);
|
|
218
|
+
try {
|
|
219
|
+
return new RegExp(source, flags.includes('i') ? flags : `${flags}i`);
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const escaped = trimmed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
226
|
+
return new RegExp(escaped, 'i');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function normalizeMaterialRoleOverrides(overrides = null) {
|
|
230
|
+
if (!overrides) return EMPTY_ROLE_OVERRIDES;
|
|
231
|
+
|
|
232
|
+
const byName = mapEntriesToRoleMap(overrides.byName ?? overrides.names);
|
|
233
|
+
const byUuid = mapEntriesToRoleMap(overrides.byUuid ?? overrides.uuids);
|
|
234
|
+
const reservedKeys = new Set(['byName', 'names', 'byUuid', 'uuids', 'patterns']);
|
|
235
|
+
|
|
236
|
+
for (const [key, role] of Object.entries(overrides)) {
|
|
237
|
+
if (reservedKeys.has(key)) continue;
|
|
238
|
+
byName.set(key, normalizeMaterialRole(role));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const patterns = (overrides.patterns ?? [])
|
|
242
|
+
.map((entry) => {
|
|
243
|
+
if (!entry) return null;
|
|
244
|
+
const pattern = normalizePattern(entry.pattern ?? entry.match ?? entry.name);
|
|
245
|
+
const role = normalizeMaterialRole(entry.role);
|
|
246
|
+
return pattern ? { pattern, role } : null;
|
|
247
|
+
})
|
|
248
|
+
.filter(Boolean);
|
|
249
|
+
|
|
250
|
+
return { byName, byUuid, patterns, sourcesByUuid: new Map() };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function userDataRole(mat) {
|
|
254
|
+
const mmd = mat?.userData?.MMD || {};
|
|
255
|
+
const role = mat?.userData?.toonRole ??
|
|
256
|
+
mat?.userData?.materialRole ??
|
|
257
|
+
mat?.userData?.role ??
|
|
258
|
+
mmd.toonRole ??
|
|
259
|
+
mmd.materialRole ??
|
|
260
|
+
mmd.role;
|
|
261
|
+
return role === undefined || role === null || String(role).trim() === ''
|
|
262
|
+
? null
|
|
263
|
+
: normalizeMaterialRole(role);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function overrideRole(mat, overrides) {
|
|
267
|
+
if (!overrides || overrides === EMPTY_ROLE_OVERRIDES) return null;
|
|
268
|
+
|
|
269
|
+
if (mat?.uuid && overrides.byUuid.has(mat.uuid)) {
|
|
270
|
+
return {
|
|
271
|
+
role: overrides.byUuid.get(mat.uuid),
|
|
272
|
+
source: overrides.sourcesByUuid?.get(mat.uuid) ?? 'override:uuid',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const mmd = mat?.userData?.MMD || {};
|
|
277
|
+
const names = [mat?.name, mmd.name, mmd.englishName].filter(Boolean);
|
|
278
|
+
for (const name of names) {
|
|
279
|
+
if (overrides.byName.has(name)) return { role: overrides.byName.get(name), source: 'override:name' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const text = materialText(mat);
|
|
283
|
+
for (const { pattern, role } of overrides.patterns) {
|
|
284
|
+
if (pattern.test(text)) return { role, source: 'override:pattern' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function flagsForPrimaryRole(primaryRole) {
|
|
291
|
+
return {
|
|
292
|
+
isBlush: primaryRole === 'blush',
|
|
293
|
+
isCatchlight: primaryRole === 'catchlight',
|
|
294
|
+
isEye: primaryRole === 'eye' ||
|
|
295
|
+
primaryRole === 'eyeHighlight' ||
|
|
296
|
+
primaryRole === 'iris' ||
|
|
297
|
+
primaryRole === 'pupil' ||
|
|
298
|
+
primaryRole === 'sclera' ||
|
|
299
|
+
primaryRole === 'catchlight',
|
|
300
|
+
isEyeHighlight: primaryRole === 'eyeHighlight' || primaryRole === 'catchlight',
|
|
301
|
+
isFace: primaryRole === 'face' || primaryRole === 'blush',
|
|
302
|
+
isHair: primaryRole === 'hair',
|
|
303
|
+
isIris: primaryRole === 'iris',
|
|
304
|
+
isMetal: primaryRole === 'metal',
|
|
305
|
+
isPupil: primaryRole === 'pupil',
|
|
306
|
+
isSclera: primaryRole === 'sclera',
|
|
307
|
+
isSkin: primaryRole === 'skin',
|
|
308
|
+
isTransparentOverlay: primaryRole === 'transparentOverlay' ||
|
|
309
|
+
primaryRole === 'eyeHighlight' ||
|
|
310
|
+
primaryRole === 'catchlight' ||
|
|
311
|
+
primaryRole === 'blush',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function classifyMaterialHeuristics(mat) {
|
|
316
|
+
const text = materialText(mat);
|
|
317
|
+
const isCatchlight = hasAnyToken(text, CATCHLIGHT_TOKENS);
|
|
318
|
+
const isEyeHighlight = hasAnyToken(text, EYE_HIGHLIGHT_TOKENS);
|
|
319
|
+
const isBlush = hasAnyToken(text, BLUSH_TOKENS);
|
|
320
|
+
const isTransparentOverlay = isEyeHighlight ||
|
|
321
|
+
isCatchlight ||
|
|
322
|
+
isBlush ||
|
|
323
|
+
hasAnyToken(text, TRANSPARENT_OVERLAY_TOKENS);
|
|
324
|
+
const isIris = hasAnyToken(text, IRIS_TOKENS);
|
|
325
|
+
const isPupil = hasAnyToken(text, PUPIL_TOKENS);
|
|
326
|
+
const isSclera = hasAnyToken(text, SCLERA_TOKENS);
|
|
327
|
+
const isEye = hasAnyToken(text, EYE_TOKENS);
|
|
328
|
+
const isFace = hasAnyToken(text, FACE_TOKENS);
|
|
329
|
+
const isSkin = hasAnyToken(text, SKIN_TOKENS);
|
|
330
|
+
const isHair = hasAnyToken(text, HAIR_TOKENS);
|
|
331
|
+
const isMetal = hasAnyToken(text, METAL_TOKENS);
|
|
332
|
+
const isCostume = hasAnyToken(text, COSTUME_TOKENS);
|
|
333
|
+
|
|
334
|
+
let primaryRole = 'default';
|
|
335
|
+
if (isCatchlight) primaryRole = 'catchlight';
|
|
336
|
+
else if (isEyeHighlight) primaryRole = 'eyeHighlight';
|
|
337
|
+
else if (isBlush) primaryRole = 'blush';
|
|
338
|
+
else if (isTransparentOverlay) primaryRole = 'transparentOverlay';
|
|
339
|
+
else if (isPupil) primaryRole = 'pupil';
|
|
340
|
+
else if (isIris) primaryRole = 'iris';
|
|
341
|
+
else if (isSclera) primaryRole = 'sclera';
|
|
342
|
+
else if (isEye) primaryRole = 'eye';
|
|
343
|
+
else if (isFace) primaryRole = 'face';
|
|
344
|
+
else if (isSkin) primaryRole = 'skin';
|
|
345
|
+
else if (isHair) primaryRole = 'hair';
|
|
346
|
+
else if (isMetal) primaryRole = 'metal';
|
|
347
|
+
else if (isCostume) primaryRole = 'costume';
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
isBlush,
|
|
351
|
+
isCatchlight,
|
|
352
|
+
isCostume,
|
|
353
|
+
isEye: isEye || isEyeHighlight || isCatchlight || isIris || isPupil || isSclera,
|
|
354
|
+
isEyeHighlight: isEyeHighlight || isCatchlight,
|
|
355
|
+
isFace,
|
|
356
|
+
isHair,
|
|
357
|
+
isIris,
|
|
358
|
+
isMetal,
|
|
359
|
+
isPupil,
|
|
360
|
+
isSclera,
|
|
361
|
+
isSkin,
|
|
362
|
+
isTransparentOverlay,
|
|
363
|
+
primaryRole,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function classifyMaterialRole(mat, overrides = null) {
|
|
368
|
+
const normalizedOverrides = overrides?.byName instanceof Map
|
|
369
|
+
? overrides
|
|
370
|
+
: normalizeMaterialRoleOverrides(overrides);
|
|
371
|
+
const userRole = userDataRole(mat);
|
|
372
|
+
const explicitOverride = userRole
|
|
373
|
+
? { role: userRole, source: 'userData' }
|
|
374
|
+
: overrideRole(mat, normalizedOverrides);
|
|
375
|
+
|
|
376
|
+
if (explicitOverride) {
|
|
377
|
+
const role = normalizeMaterialRole(explicitOverride.role);
|
|
378
|
+
return {
|
|
379
|
+
...flagsForPrimaryRole(role),
|
|
380
|
+
isCostume: role === 'costume',
|
|
381
|
+
primaryRole: role,
|
|
382
|
+
role,
|
|
383
|
+
roleLabel: MATERIAL_ROLE_LABELS[role],
|
|
384
|
+
roleValue: MATERIAL_ROLES[role],
|
|
385
|
+
source: explicitOverride.source,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const heuristic = classifyMaterialHeuristics(mat);
|
|
390
|
+
const role = heuristic.primaryRole;
|
|
391
|
+
return {
|
|
392
|
+
...heuristic,
|
|
393
|
+
role,
|
|
394
|
+
roleLabel: MATERIAL_ROLE_LABELS[role],
|
|
395
|
+
roleValue: MATERIAL_ROLES[role],
|
|
396
|
+
source: 'heuristic',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function roleIsTransparentOverlay(roleInfo) {
|
|
401
|
+
return Boolean(roleInfo?.isTransparentOverlay);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function roleIsEyeHighlight(roleInfo) {
|
|
405
|
+
return Boolean(roleInfo?.isEyeHighlight);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function roleIsCatchlight(roleInfo) {
|
|
409
|
+
return Boolean(roleInfo?.isCatchlight);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function roleIsEye(roleInfo) {
|
|
413
|
+
return Boolean(roleInfo?.isEye);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function roleIsIris(roleInfo) {
|
|
417
|
+
return Boolean(roleInfo?.isIris);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function roleIsPupil(roleInfo) {
|
|
421
|
+
return Boolean(roleInfo?.isPupil);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function roleIsSclera(roleInfo) {
|
|
425
|
+
return Boolean(roleInfo?.isSclera);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function roleIsFace(roleInfo) {
|
|
429
|
+
return Boolean(roleInfo?.isFace);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function roleIsSkin(roleInfo) {
|
|
433
|
+
return Boolean(roleInfo?.isSkin);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function roleIsHair(roleInfo) {
|
|
437
|
+
return Boolean(roleInfo?.isHair);
|
|
438
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export function isPlainPresetObject(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function cleanPresetObject(value) {
|
|
6
|
+
return isPlainPresetObject(value) ? value : {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function invalidPresetResult(message, warnings = []) {
|
|
10
|
+
return {
|
|
11
|
+
errors: [message],
|
|
12
|
+
ok: false,
|
|
13
|
+
value: null,
|
|
14
|
+
warnings,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parsePresetDocument(input, validateDocument, { invalidJsonLabel = 'preset' } = {}) {
|
|
19
|
+
try {
|
|
20
|
+
const source = typeof input === 'string' ? JSON.parse(input) : input;
|
|
21
|
+
return validateDocument(source);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return invalidPresetResult(`Invalid ${invalidJsonLabel} JSON: ${error.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function validateSettingsPresetDocument(input, {
|
|
28
|
+
collectWarnings = () => [],
|
|
29
|
+
documentType,
|
|
30
|
+
normalizeId,
|
|
31
|
+
sanitizeSettings,
|
|
32
|
+
schemaVersion,
|
|
33
|
+
migrateDocument,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const warnings = [];
|
|
36
|
+
|
|
37
|
+
if (!isPlainPresetObject(input)) {
|
|
38
|
+
return {
|
|
39
|
+
errors: ['Preset document must be a JSON object.'],
|
|
40
|
+
ok: false,
|
|
41
|
+
value: null,
|
|
42
|
+
warnings,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const migrated = migrateDocument(input);
|
|
47
|
+
if (migrated.type !== documentType) {
|
|
48
|
+
warnings.push(`Preset type "${migrated.type}" was normalized to "${documentType}".`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const errors = [];
|
|
52
|
+
if (migrated.version > schemaVersion) {
|
|
53
|
+
errors.push(`Preset schema version ${migrated.version} is newer than supported version ${schemaVersion}.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const presetId = normalizeId(migrated.id);
|
|
57
|
+
if (!presetId) errors.push('Preset id is required.');
|
|
58
|
+
|
|
59
|
+
const settings = cleanPresetObject(migrated.settings);
|
|
60
|
+
warnings.push(...collectWarnings(settings));
|
|
61
|
+
|
|
62
|
+
const value = errors.length === 0
|
|
63
|
+
? {
|
|
64
|
+
description: String(migrated.description ?? ''),
|
|
65
|
+
id: presetId,
|
|
66
|
+
label: String(migrated.label || presetId),
|
|
67
|
+
settings: sanitizeSettings(settings),
|
|
68
|
+
type: documentType,
|
|
69
|
+
version: schemaVersion,
|
|
70
|
+
}
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
errors,
|
|
75
|
+
ok: errors.length === 0,
|
|
76
|
+
value,
|
|
77
|
+
warnings,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createSettingsPresetDocument(id, definition = {}, {
|
|
82
|
+
collectSettings,
|
|
83
|
+
documentType,
|
|
84
|
+
schemaVersion,
|
|
85
|
+
validateDocument,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const source = cleanPresetObject(definition);
|
|
88
|
+
const document = {
|
|
89
|
+
description: source.description ?? '',
|
|
90
|
+
id: id ?? source.id ?? source.name ?? source.preset,
|
|
91
|
+
label: source.label ?? source.title ?? source.name ?? id,
|
|
92
|
+
settings: collectSettings(source),
|
|
93
|
+
type: documentType,
|
|
94
|
+
version: schemaVersion,
|
|
95
|
+
};
|
|
96
|
+
const result = validateDocument(document);
|
|
97
|
+
if (!result.ok) throw new Error(result.errors.join(' '));
|
|
98
|
+
return result.value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function serializePresetDocument(idOrDocument, definition = {}, {
|
|
102
|
+
argumentCount = 2,
|
|
103
|
+
createDocument,
|
|
104
|
+
pretty = true,
|
|
105
|
+
} = {}) {
|
|
106
|
+
const document = isPlainPresetObject(idOrDocument) && argumentCount <= 2
|
|
107
|
+
? createDocument(idOrDocument.id, idOrDocument)
|
|
108
|
+
: createDocument(idOrDocument, definition);
|
|
109
|
+
return JSON.stringify(document, null, pretty ? 2 : 0);
|
|
110
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Active shader backend for the material factories in src/.
|
|
2
|
+
//
|
|
3
|
+
// 'tsl' — WebGPURenderer (WebGPU or forced-WebGL2 backend) + NodeMaterial.
|
|
4
|
+
//
|
|
5
|
+
// The renderer factory sets this at boot, BEFORE any material factory runs.
|
|
6
|
+
// TSL is also the module default so setup utilities that create materials
|
|
7
|
+
// before renderer construction still land on the supported path.
|
|
8
|
+
|
|
9
|
+
let activeShaderBackend = 'tsl';
|
|
10
|
+
|
|
11
|
+
export function setActiveShaderBackend(backend) {
|
|
12
|
+
if (backend !== 'tsl') {
|
|
13
|
+
console.warn(`Unsupported shader backend "${backend}"; using TSL.`);
|
|
14
|
+
}
|
|
15
|
+
activeShaderBackend = 'tsl';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getActiveShaderBackend() {
|
|
19
|
+
return activeShaderBackend;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isTslBackend() {
|
|
23
|
+
return true;
|
|
24
|
+
}
|