@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,433 @@
|
|
|
1
|
+
import { FileLoader, LoadingManager } from 'three';
|
|
2
|
+
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
|
|
3
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
4
|
+
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
|
|
5
|
+
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
|
6
|
+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
7
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
8
|
+
import { USDZLoader } from 'three/examples/jsm/loaders/USDZLoader.js';
|
|
9
|
+
import { unzipSync } from 'three/examples/jsm/libs/fflate.module.js';
|
|
10
|
+
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
|
11
|
+
import { MMDLoader } from 'three-stdlib';
|
|
12
|
+
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
|
|
13
|
+
|
|
14
|
+
export const SUPPORTED_MODEL_FORMATS = ['pmx', 'pmd', 'fbx', 'glb', 'gltf', 'vrm', 'obj', 'usdz'];
|
|
15
|
+
|
|
16
|
+
const BASE_URL = import.meta.env?.BASE_URL || '/';
|
|
17
|
+
const TEXT_DECODER_LABELS_FOR_MOJIBAKE = ['macintosh', 'windows-1252'];
|
|
18
|
+
const SOURCE_TEXT_ENCODINGS_FOR_MOJIBAKE = ['shift_jis', 'gbk', 'big5'];
|
|
19
|
+
|
|
20
|
+
const additionalModelTextureAssetPaths = new Set();
|
|
21
|
+
const textEncodeMaps = new Map();
|
|
22
|
+
|
|
23
|
+
function safeDecodeURIComponent(value) {
|
|
24
|
+
try {
|
|
25
|
+
return decodeURIComponent(value);
|
|
26
|
+
} catch {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeAssetPath(url) {
|
|
32
|
+
const cleanUrl = stripUrlSuffix(String(url || '')).replace(/\\/g, '/');
|
|
33
|
+
const withoutOrigin = cleanUrl.replace(/^[a-z]+:\/\/[^/]+/i, '');
|
|
34
|
+
const basePath = BASE_URL === '/' ? '/' : BASE_URL;
|
|
35
|
+
const withoutBase = withoutOrigin.startsWith(basePath)
|
|
36
|
+
? withoutOrigin.slice(basePath.length)
|
|
37
|
+
: withoutOrigin;
|
|
38
|
+
|
|
39
|
+
return safeDecodeURIComponent(withoutBase).replace(/^\/+/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addPathVariant(map, path, resolvedPath) {
|
|
43
|
+
const normalizedPath = normalizeAssetPath(path);
|
|
44
|
+
if (normalizedPath) map.set(normalizedPath, resolvedPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildModelTextureAssetIndex() {
|
|
48
|
+
const byPath = new Map();
|
|
49
|
+
const byDirectory = new Map();
|
|
50
|
+
|
|
51
|
+
for (const sourcePath of additionalModelTextureAssetPaths) {
|
|
52
|
+
addPathVariant(byPath, sourcePath, sourcePath);
|
|
53
|
+
|
|
54
|
+
const normalizedSourcePath = normalizeAssetPath(sourcePath);
|
|
55
|
+
const slashIndex = normalizedSourcePath.lastIndexOf('/');
|
|
56
|
+
if (slashIndex === -1) continue;
|
|
57
|
+
|
|
58
|
+
const directory = normalizedSourcePath.slice(0, slashIndex);
|
|
59
|
+
const filename = normalizedSourcePath.slice(slashIndex + 1);
|
|
60
|
+
const files = byDirectory.get(directory) ?? new Map();
|
|
61
|
+
files.set(filename, sourcePath);
|
|
62
|
+
byDirectory.set(directory, files);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { byDirectory, byPath };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let MODEL_TEXTURE_ASSET_INDEX = buildModelTextureAssetIndex();
|
|
69
|
+
|
|
70
|
+
export function registerModelTextureAssetPaths(paths) {
|
|
71
|
+
for (const path of paths ?? []) {
|
|
72
|
+
if (path) additionalModelTextureAssetPaths.add(path);
|
|
73
|
+
}
|
|
74
|
+
MODEL_TEXTURE_ASSET_INDEX = buildModelTextureAssetIndex();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stripUrlSuffix(url) {
|
|
78
|
+
return url.split(/[?#]/)[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getModelFormat(url) {
|
|
82
|
+
const cleanUrl = stripUrlSuffix(url).toLowerCase();
|
|
83
|
+
const extension = cleanUrl.slice(cleanUrl.lastIndexOf('.') + 1);
|
|
84
|
+
if (extension === 'pmx' || extension === 'pmd') return 'mmd';
|
|
85
|
+
if (extension === 'fbx') return 'fbx';
|
|
86
|
+
if (extension === 'glb' || extension === 'gltf') return 'gltf';
|
|
87
|
+
if (extension === 'vrm') return 'vrm';
|
|
88
|
+
if (extension === 'obj') return 'obj';
|
|
89
|
+
if (extension === 'usdz') return 'usdz';
|
|
90
|
+
return extension || 'unknown';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getResourcePath(url) {
|
|
94
|
+
const cleanUrl = stripUrlSuffix(url);
|
|
95
|
+
const slashIndex = cleanUrl.lastIndexOf('/');
|
|
96
|
+
return slashIndex === -1 ? './' : cleanUrl.slice(0, slashIndex + 1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function loadAsync(loader, url) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
loader.load(url, resolve, undefined, reject);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function loadArrayBuffer(url, manager = undefined) {
|
|
106
|
+
const loader = new FileLoader(manager);
|
|
107
|
+
loader.setResponseType('arraybuffer');
|
|
108
|
+
return loadAsync(loader, url);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getTextEncodeMap(label) {
|
|
112
|
+
const cachedMap = textEncodeMaps.get(label);
|
|
113
|
+
if (cachedMap) return cachedMap;
|
|
114
|
+
|
|
115
|
+
const encodeMap = new Map();
|
|
116
|
+
|
|
117
|
+
let decoder;
|
|
118
|
+
try {
|
|
119
|
+
decoder = new TextDecoder(label);
|
|
120
|
+
} catch {
|
|
121
|
+
textEncodeMaps.set(label, encodeMap);
|
|
122
|
+
return encodeMap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function addDecodedBytes(bytes) {
|
|
126
|
+
const decoded = decoder.decode(Uint8Array.from(bytes));
|
|
127
|
+
if (!decoded || decoded.includes('\uFFFD') || encodeMap.has(decoded)) return;
|
|
128
|
+
encodeMap.set(decoded, bytes);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (let byte = 0x00; byte <= 0x7f; byte += 1) addDecodedBytes([byte]);
|
|
132
|
+
for (let byte = 0x80; byte <= 0xff; byte += 1) addDecodedBytes([byte]);
|
|
133
|
+
|
|
134
|
+
for (let lead = 0x81; lead <= 0xfe; lead += 1) {
|
|
135
|
+
for (let trail = 0x40; trail <= 0xfe; trail += 1) {
|
|
136
|
+
if (trail === 0x7f) continue;
|
|
137
|
+
addDecodedBytes([lead, trail]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
textEncodeMaps.set(label, encodeMap);
|
|
142
|
+
return encodeMap;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function encodeText(value, label) {
|
|
146
|
+
const encoderMap = getTextEncodeMap(label);
|
|
147
|
+
const bytes = [];
|
|
148
|
+
|
|
149
|
+
for (const char of value) {
|
|
150
|
+
const encoded = encoderMap.get(char);
|
|
151
|
+
if (!encoded) return null;
|
|
152
|
+
bytes.push(...encoded);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Uint8Array.from(bytes);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function decodeBytes(bytes, label) {
|
|
159
|
+
try {
|
|
160
|
+
return new TextDecoder(label).decode(bytes);
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getMojibakeFilenameCandidates(filename) {
|
|
167
|
+
const candidates = [];
|
|
168
|
+
|
|
169
|
+
for (const sourceEncoding of SOURCE_TEXT_ENCODINGS_FOR_MOJIBAKE) {
|
|
170
|
+
const sourceBytes = encodeText(filename, sourceEncoding);
|
|
171
|
+
if (!sourceBytes) continue;
|
|
172
|
+
|
|
173
|
+
for (const brokenEncoding of TEXT_DECODER_LABELS_FOR_MOJIBAKE) {
|
|
174
|
+
const candidate = decodeBytes(sourceBytes, brokenEncoding);
|
|
175
|
+
if (candidate && candidate !== filename && !candidates.includes(candidate)) {
|
|
176
|
+
candidates.push(candidate);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return candidates;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveKnownModelAssetUrl(url) {
|
|
185
|
+
const normalizedPath = normalizeAssetPath(url);
|
|
186
|
+
const exactAssetUrl = MODEL_TEXTURE_ASSET_INDEX.byPath.get(normalizedPath);
|
|
187
|
+
if (exactAssetUrl) return exactAssetUrl;
|
|
188
|
+
|
|
189
|
+
const slashIndex = normalizedPath.lastIndexOf('/');
|
|
190
|
+
if (slashIndex === -1) return null;
|
|
191
|
+
|
|
192
|
+
const directory = normalizedPath.slice(0, slashIndex);
|
|
193
|
+
const filename = normalizedPath.slice(slashIndex + 1);
|
|
194
|
+
const files = MODEL_TEXTURE_ASSET_INDEX.byDirectory.get(directory);
|
|
195
|
+
if (!files) return null;
|
|
196
|
+
|
|
197
|
+
for (const candidate of getMojibakeFilenameCandidates(filename)) {
|
|
198
|
+
const assetUrl = files.get(candidate);
|
|
199
|
+
if (assetUrl) return assetUrl;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function createModelLoadingManager() {
|
|
206
|
+
const manager = new LoadingManager();
|
|
207
|
+
|
|
208
|
+
manager.setURLModifier((url) => {
|
|
209
|
+
return resolveKnownModelAssetUrl(url) ?? url;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return manager;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isBinaryUsdFile(bytes) {
|
|
216
|
+
const header = new Uint8Array(bytes.buffer, bytes.byteOffset, Math.min(bytes.byteLength, 8));
|
|
217
|
+
const crateHeader = [0x50, 0x58, 0x52, 0x2d, 0x55, 0x53, 0x44, 0x43];
|
|
218
|
+
return crateHeader.every((value, index) => header[index] === value);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateUsdzBuffer(buffer, url) {
|
|
222
|
+
const zip = unzipSync(new Uint8Array(buffer));
|
|
223
|
+
const usdEntries = Object.entries(zip).filter(([filename]) => /\.(usd|usda|usdc)$/i.test(filename));
|
|
224
|
+
|
|
225
|
+
if (usdEntries.length === 0) {
|
|
226
|
+
throw new Error(`USDZ file ${url} does not contain a USD scene file.`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const firstUsdEntry = usdEntries[0];
|
|
230
|
+
const [filename, bytes] = firstUsdEntry;
|
|
231
|
+
const isCrate = /\.usdc$/i.test(filename) || (/\.usd$/i.test(filename) && isBinaryUsdFile(bytes));
|
|
232
|
+
|
|
233
|
+
if (isCrate) {
|
|
234
|
+
throw new Error(`USDZ file ${url} uses binary USDC/crate data (${filename}). Three.js USDZLoader only supports text USDA/USD scenes.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function configureGltfLoader(loader, { decoderBasePath = null, renderer = null } = {}) {
|
|
239
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
240
|
+
|
|
241
|
+
if (!decoderBasePath) return;
|
|
242
|
+
|
|
243
|
+
const normalizedDecoderBasePath = decoderBasePath.endsWith('/')
|
|
244
|
+
? decoderBasePath
|
|
245
|
+
: `${decoderBasePath}/`;
|
|
246
|
+
|
|
247
|
+
const dracoLoader = new DRACOLoader();
|
|
248
|
+
dracoLoader.setDecoderPath(`${normalizedDecoderBasePath}draco/`);
|
|
249
|
+
loader.setDRACOLoader(dracoLoader);
|
|
250
|
+
|
|
251
|
+
if (renderer) {
|
|
252
|
+
const ktx2Loader = new KTX2Loader();
|
|
253
|
+
ktx2Loader.setTranscoderPath(`${normalizedDecoderBasePath}basis/`);
|
|
254
|
+
ktx2Loader.detectSupport(renderer);
|
|
255
|
+
loader.setKTX2Loader(ktx2Loader);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function createModelAsset({ clips = [], format, materialUrl = null, resourcePath, root, url, vrm = null }) {
|
|
260
|
+
return {
|
|
261
|
+
clips,
|
|
262
|
+
format,
|
|
263
|
+
materialUrl,
|
|
264
|
+
resourcePath,
|
|
265
|
+
root,
|
|
266
|
+
url,
|
|
267
|
+
vrm,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function toMaterialArray(material) {
|
|
272
|
+
if (!material) return [];
|
|
273
|
+
return Array.isArray(material) ? material : [material];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function annotateGltfSourceMetadata(gltf, url) {
|
|
277
|
+
const parser = gltf?.parser;
|
|
278
|
+
const json = parser?.json;
|
|
279
|
+
const associations = parser?.associations;
|
|
280
|
+
if (!json || !associations || !gltf?.scene) return;
|
|
281
|
+
|
|
282
|
+
const materials = new Set();
|
|
283
|
+
gltf.scene.traverse((obj) => {
|
|
284
|
+
if (!obj.isMesh || !obj.material) return;
|
|
285
|
+
for (const material of toMaterialArray(obj.material)) materials.add(material);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
for (const material of materials) {
|
|
289
|
+
const association = associations.get(material);
|
|
290
|
+
const materialIndex = association?.materials;
|
|
291
|
+
if (!Number.isInteger(materialIndex)) continue;
|
|
292
|
+
|
|
293
|
+
const materialDef = json.materials?.[materialIndex] || {};
|
|
294
|
+
const baseColorTextureIndex = materialDef.pbrMetallicRoughness?.baseColorTexture?.index;
|
|
295
|
+
const textureDef = Number.isInteger(baseColorTextureIndex)
|
|
296
|
+
? json.textures?.[baseColorTextureIndex]
|
|
297
|
+
: null;
|
|
298
|
+
const imageIndex = Number.isInteger(textureDef?.source) ? textureDef.source : null;
|
|
299
|
+
const imageDef = Number.isInteger(imageIndex) ? json.images?.[imageIndex] : null;
|
|
300
|
+
const textureName = textureDef?.name || '';
|
|
301
|
+
const imageName = imageDef?.name || '';
|
|
302
|
+
const imageUri = imageDef?.uri || '';
|
|
303
|
+
|
|
304
|
+
material.userData.toonSource = {
|
|
305
|
+
...(material.userData.toonSource || {}),
|
|
306
|
+
baseColorTextureIndex: Number.isInteger(baseColorTextureIndex) ? baseColorTextureIndex : null,
|
|
307
|
+
format: 'gltf',
|
|
308
|
+
imageCount: json.images?.length ?? 0,
|
|
309
|
+
imageIndex,
|
|
310
|
+
imageName,
|
|
311
|
+
imageUri,
|
|
312
|
+
materialIndex,
|
|
313
|
+
materialName: materialDef.name || '',
|
|
314
|
+
sourceUrl: url,
|
|
315
|
+
textureName,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (material.map) {
|
|
319
|
+
material.map.userData.sourceName = textureName || imageName || '';
|
|
320
|
+
material.map.userData.sourceUri = imageUri || '';
|
|
321
|
+
material.map.userData.gltfImageName = imageName || '';
|
|
322
|
+
material.map.userData.gltfImageUri = imageUri || '';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function loadModelAsset(url, {
|
|
328
|
+
decoderBasePath = null,
|
|
329
|
+
materialUrl = null,
|
|
330
|
+
renderer = null,
|
|
331
|
+
} = {}) {
|
|
332
|
+
const format = getModelFormat(url);
|
|
333
|
+
const resourcePath = getResourcePath(url);
|
|
334
|
+
const manager = createModelLoadingManager();
|
|
335
|
+
|
|
336
|
+
if (format === 'mmd') {
|
|
337
|
+
const loader = new MMDLoader(manager);
|
|
338
|
+
loader.setResourcePath(resourcePath);
|
|
339
|
+
const root = await loadAsync(loader, url);
|
|
340
|
+
|
|
341
|
+
return createModelAsset({
|
|
342
|
+
clips: root.animations || [],
|
|
343
|
+
format: 'mmd',
|
|
344
|
+
resourcePath,
|
|
345
|
+
root,
|
|
346
|
+
url,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (format === 'fbx') {
|
|
351
|
+
const loader = new FBXLoader(manager);
|
|
352
|
+
loader.setResourcePath(resourcePath);
|
|
353
|
+
const root = await loadAsync(loader, url);
|
|
354
|
+
|
|
355
|
+
return createModelAsset({
|
|
356
|
+
clips: root.animations || [],
|
|
357
|
+
format,
|
|
358
|
+
resourcePath,
|
|
359
|
+
root,
|
|
360
|
+
url,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (format === 'gltf' || format === 'vrm') {
|
|
365
|
+
const loader = new GLTFLoader(manager);
|
|
366
|
+
loader.setResourcePath(resourcePath);
|
|
367
|
+
configureGltfLoader(loader, { decoderBasePath, renderer });
|
|
368
|
+
// The VRM plugin only activates when the glTF carries the VRM extension,
|
|
369
|
+
// so registering it for every glTF load is safe.
|
|
370
|
+
loader.register((parser) => new VRMLoaderPlugin(parser));
|
|
371
|
+
const gltf = await loadAsync(loader, url);
|
|
372
|
+
annotateGltfSourceMetadata(gltf, url);
|
|
373
|
+
|
|
374
|
+
const vrm = gltf.userData?.vrm ?? null;
|
|
375
|
+
if (vrm) {
|
|
376
|
+
// VRM0 models face +Z after this, matching VRM1/three convention.
|
|
377
|
+
VRMUtils.rotateVRM0(vrm);
|
|
378
|
+
// Clips animate the raw scene bones directly; without this vrm.update()
|
|
379
|
+
// would overwrite them from the untouched normalized rig every frame.
|
|
380
|
+
vrm.humanoid.autoUpdateHumanBones = false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return createModelAsset({
|
|
384
|
+
clips: gltf.animations || [],
|
|
385
|
+
format: vrm ? 'vrm' : 'gltf',
|
|
386
|
+
resourcePath,
|
|
387
|
+
root: gltf.scene,
|
|
388
|
+
url,
|
|
389
|
+
vrm,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (format === 'obj') {
|
|
394
|
+
const loader = new OBJLoader(manager);
|
|
395
|
+
loader.setResourcePath(resourcePath);
|
|
396
|
+
|
|
397
|
+
if (materialUrl) {
|
|
398
|
+
const materialResourcePath = getResourcePath(materialUrl);
|
|
399
|
+
const mtlLoader = new MTLLoader(manager);
|
|
400
|
+
mtlLoader.setResourcePath(materialResourcePath);
|
|
401
|
+
const materials = await loadAsync(mtlLoader, materialUrl);
|
|
402
|
+
materials.preload();
|
|
403
|
+
loader.setMaterials(materials);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const root = await loadAsync(loader, url);
|
|
407
|
+
|
|
408
|
+
return createModelAsset({
|
|
409
|
+
clips: root.animations || [],
|
|
410
|
+
format,
|
|
411
|
+
materialUrl,
|
|
412
|
+
resourcePath,
|
|
413
|
+
root,
|
|
414
|
+
url,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (format === 'usdz') {
|
|
419
|
+
const loader = new USDZLoader();
|
|
420
|
+
const buffer = await loadArrayBuffer(url, manager);
|
|
421
|
+
validateUsdzBuffer(buffer, url);
|
|
422
|
+
const root = loader.parse(buffer);
|
|
423
|
+
|
|
424
|
+
return createModelAsset({
|
|
425
|
+
format,
|
|
426
|
+
resourcePath,
|
|
427
|
+
root,
|
|
428
|
+
url,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
throw new Error(`Unsupported model format for ${url}. Supported formats: ${SUPPORTED_MODEL_FORMATS.join(', ')}.`);
|
|
433
|
+
}
|