@call-me-sensei/toonlab 0.2.0 → 0.3.0
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/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMotionClipSlots,
|
|
3
|
+
createMotionGraph,
|
|
4
|
+
evaluateMotionCondition,
|
|
5
|
+
validateMotionGraph,
|
|
6
|
+
} from './motionGraph.js';
|
|
7
|
+
import {
|
|
8
|
+
blendMotionPoseList,
|
|
9
|
+
blendMotionPoses,
|
|
10
|
+
createEmptyMotionPose,
|
|
11
|
+
layerMotionPose,
|
|
12
|
+
normalizeMotionPose,
|
|
13
|
+
} from './motionClip.js';
|
|
14
|
+
import { createMotionSettings } from './motionSettings.js';
|
|
15
|
+
|
|
16
|
+
const plain = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
|
|
18
|
+
const clamp01 = (value) => Math.min(Math.max(finite(value), 0), 1);
|
|
19
|
+
|
|
20
|
+
const EMPTY_CLIP = Object.freeze({
|
|
21
|
+
id: 'missing-clip',
|
|
22
|
+
duration: 1,
|
|
23
|
+
events: Object.freeze([]),
|
|
24
|
+
sample: () => createEmptyMotionPose(),
|
|
25
|
+
dispose() {},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function now() {
|
|
29
|
+
return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ease(value, mode) {
|
|
33
|
+
const t = clamp01(value);
|
|
34
|
+
if (mode === 'smoothstep') return t * t * (3 - 2 * t);
|
|
35
|
+
if (mode === 'easeInOutCubic') return t < 0.5 ? 4 * t ** 3 : 1 - ((-2 * t + 2) ** 3) / 2;
|
|
36
|
+
return t;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function addWeights(entries) {
|
|
40
|
+
const total = entries.reduce((sum, entry) => sum + Math.max(0, finite(entry.weight, 0)), 0);
|
|
41
|
+
if (total <= 0) return entries.map((entry) => ({ ...entry, weight: 0 }));
|
|
42
|
+
return entries.map((entry) => ({ ...entry, weight: Math.max(0, finite(entry.weight, 0)) / total }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mergeContributions(entries) {
|
|
46
|
+
const map = new Map();
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const key = entry.key;
|
|
49
|
+
const previous = map.get(key);
|
|
50
|
+
if (previous) previous.weight += entry.weight;
|
|
51
|
+
else map.set(key, { ...entry });
|
|
52
|
+
}
|
|
53
|
+
return [...map.values()];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clipDuration(sampler) {
|
|
57
|
+
return Math.max(0.001, finite(sampler?.duration, 1));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clipSourceKey(source) {
|
|
61
|
+
if (typeof source === 'string' || typeof source === 'number') return String(source);
|
|
62
|
+
if (plain(source)) return String(source.id ?? source.uri ?? source.url ?? source.asset ?? '');
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeSampler(value, fallbackId = 'clip') {
|
|
67
|
+
if (!value || typeof value.sample !== 'function') return EMPTY_CLIP;
|
|
68
|
+
return {
|
|
69
|
+
id: String(value.id ?? fallbackId),
|
|
70
|
+
duration: clipDuration(value),
|
|
71
|
+
events: Array.isArray(value.events) ? value.events : [],
|
|
72
|
+
sample: value.sample.bind(value),
|
|
73
|
+
dispose: typeof value.dispose === 'function' ? value.dispose.bind(value) : () => {},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parameterValue(parameters, id, fallback = 0) {
|
|
78
|
+
return id && Object.hasOwn(parameters, id) ? parameters[id] : fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function transitionExitReady(transition, phase) {
|
|
82
|
+
return transition.exitTime === null || phase >= transition.exitTime;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function eventTime(event, duration) {
|
|
86
|
+
const time = finite(event.time ?? event.at, 0);
|
|
87
|
+
return Math.min(Math.max(event.normalized ? time * duration : time, 0), duration);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function eventIdentity(event, index) {
|
|
91
|
+
return String(event.id ?? `${event.name ?? event.type ?? 'event'}-${index}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getRootTransform(pose) {
|
|
95
|
+
return normalizeMotionPose(pose).root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function subtractVector(a, b) {
|
|
99
|
+
return a.map((value, index) => value - (b[index] ?? 0));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addScaledVector(target, source, amount) {
|
|
103
|
+
for (let index = 0; index < 3; index += 1) target[index] += finite(source[index], 0) * amount;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function shortestAngleDelta(from, to) {
|
|
107
|
+
let delta = (finite(to, 0) - finite(from, 0)) % (Math.PI * 2);
|
|
108
|
+
if (delta > Math.PI) delta -= Math.PI * 2;
|
|
109
|
+
if (delta < -Math.PI) delta += Math.PI * 2;
|
|
110
|
+
return delta;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function motionAtAbsoluteTime(sampler, absoluteTime, loop) {
|
|
114
|
+
const duration = clipDuration(sampler);
|
|
115
|
+
const time = Math.max(0, finite(absoluteTime, 0));
|
|
116
|
+
if (!loop) {
|
|
117
|
+
const root = getRootTransform(sampler.sample(Math.min(time, duration), { loop: false }));
|
|
118
|
+
return { position: root.position, yaw: root.rotation[1] };
|
|
119
|
+
}
|
|
120
|
+
const cycleStart = getRootTransform(sampler.sample(0, { loop: false }));
|
|
121
|
+
const cycleEnd = getRootTransform(sampler.sample(duration, { loop: false }));
|
|
122
|
+
const cycleDelta = subtractVector(cycleEnd.position, cycleStart.position);
|
|
123
|
+
const cycleYawDelta = shortestAngleDelta(cycleStart.rotation[1], cycleEnd.rotation[1]);
|
|
124
|
+
const cycles = Math.floor(time / duration);
|
|
125
|
+
const local = time - cycles * duration;
|
|
126
|
+
const root = getRootTransform(sampler.sample(local, { loop: false }));
|
|
127
|
+
return {
|
|
128
|
+
position: root.position.map((value, index) => value + cycleDelta[index] * cycles),
|
|
129
|
+
yaw: cycleStart.rotation[1]
|
|
130
|
+
+ cycleYawDelta * cycles
|
|
131
|
+
+ shortestAngleDelta(cycleStart.rotation[1], root.rotation[1]),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reusable graph runtime. It has no animation-name enum and no Three.js
|
|
137
|
+
* dependency: arbitrary samplers and a rig adapter are injected by the host.
|
|
138
|
+
*/
|
|
139
|
+
export function createMotionController({
|
|
140
|
+
graph: graphOptions,
|
|
141
|
+
clipSlots: clipSlotOptions,
|
|
142
|
+
clips = {},
|
|
143
|
+
resolveClip = null,
|
|
144
|
+
rig = null,
|
|
145
|
+
settings: settingOptions,
|
|
146
|
+
onEvent = null,
|
|
147
|
+
parameters = {},
|
|
148
|
+
} = {}) {
|
|
149
|
+
let graph = createMotionGraph(graphOptions);
|
|
150
|
+
let clipSlots = createMotionClipSlots(clipSlotOptions);
|
|
151
|
+
let settings = createMotionSettings(settingOptions);
|
|
152
|
+
let clipLibrary = clips;
|
|
153
|
+
let clipResolver = typeof resolveClip === 'function' ? resolveClip : null;
|
|
154
|
+
let rigAdapter = rig;
|
|
155
|
+
let disposed = false;
|
|
156
|
+
let clock = 0;
|
|
157
|
+
let stateTime = 0;
|
|
158
|
+
let currentState = graph.initial;
|
|
159
|
+
let transition = null;
|
|
160
|
+
let pose = createEmptyMotionPose();
|
|
161
|
+
let lastUpdateMs = 0;
|
|
162
|
+
let updateCount = 0;
|
|
163
|
+
let totalEventCount = 0;
|
|
164
|
+
let sampleCount = 0;
|
|
165
|
+
let leanValue = 0;
|
|
166
|
+
let squashValue = 0;
|
|
167
|
+
const triggers = new Set();
|
|
168
|
+
const eventListeners = new Set(typeof onEvent === 'function' ? [onEvent] : []);
|
|
169
|
+
const eventCursors = new Map();
|
|
170
|
+
const rootCursors = new Map();
|
|
171
|
+
const firedOnce = new Set();
|
|
172
|
+
const missingSlots = new Set();
|
|
173
|
+
const parameterValues = {};
|
|
174
|
+
|
|
175
|
+
function resetParameters(next = {}) {
|
|
176
|
+
for (const [id, definition] of Object.entries(graph.parameters)) {
|
|
177
|
+
parameterValues[id] = Object.hasOwn(next, id) ? next[id] : definition.default;
|
|
178
|
+
}
|
|
179
|
+
for (const [id, value] of Object.entries(next)) parameterValues[id] = value;
|
|
180
|
+
}
|
|
181
|
+
resetParameters(parameters);
|
|
182
|
+
|
|
183
|
+
function libraryGet(key) {
|
|
184
|
+
if (!key) return null;
|
|
185
|
+
if (clipLibrary instanceof Map) return clipLibrary.get(key) ?? null;
|
|
186
|
+
return plain(clipLibrary) ? clipLibrary[key] ?? null : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveSampler(slotId) {
|
|
190
|
+
const slot = clipSlots[slotId];
|
|
191
|
+
if (!slot) {
|
|
192
|
+
missingSlots.add(slotId || '(empty)');
|
|
193
|
+
return { sampler: EMPTY_CLIP, slot: { source: slotId, speed: 1, phase: 0, loop: true, events: [] } };
|
|
194
|
+
}
|
|
195
|
+
const sourceKey = clipSourceKey(slot.source);
|
|
196
|
+
const found = libraryGet(sourceKey) ?? libraryGet(slotId) ?? clipResolver?.(slot.source, slotId, slot) ?? null;
|
|
197
|
+
if (!found) missingSlots.add(slotId);
|
|
198
|
+
else missingSlots.delete(slotId);
|
|
199
|
+
return { sampler: normalizeSampler(found, sourceKey || slotId), slot };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sampleClipNode(node, time, prefix, weight = 1) {
|
|
203
|
+
const { sampler, slot } = resolveSampler(node.slot);
|
|
204
|
+
const duration = clipDuration(sampler);
|
|
205
|
+
const speed = Math.max(0, finite(node.speed, 1) * finite(slot.speed, 1));
|
|
206
|
+
const rawTime = time * speed + (finite(node.phase, 0) + finite(slot.phase, 0)) * duration;
|
|
207
|
+
const sampledTime = settings.playback.cadence === 'stepped'
|
|
208
|
+
? Math.floor(rawTime * settings.playback.sampleRate + 1e-7) / settings.playback.sampleRate
|
|
209
|
+
: rawTime;
|
|
210
|
+
sampleCount += 1;
|
|
211
|
+
return {
|
|
212
|
+
pose: normalizeMotionPose(sampler.sample(sampledTime, { loop: slot.loop !== false })),
|
|
213
|
+
contributions: [{
|
|
214
|
+
key: `${prefix}:${node.slot}`,
|
|
215
|
+
loop: slot.loop !== false,
|
|
216
|
+
rawTime,
|
|
217
|
+
sampler,
|
|
218
|
+
slot,
|
|
219
|
+
slotId: node.slot,
|
|
220
|
+
weight,
|
|
221
|
+
}],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function sampleNode(node, time, prefix, inheritedWeight = 1) {
|
|
226
|
+
if (!node || node.type === 'clip') return sampleClipNode(node ?? { slot: '' }, time, prefix, inheritedWeight);
|
|
227
|
+
let weightedChildren = [];
|
|
228
|
+
if (node.type === 'blend1d') {
|
|
229
|
+
const value = finite(parameterValue(parameterValues, node.parameter, 0), 0);
|
|
230
|
+
const children = node.children ?? [];
|
|
231
|
+
if (children.length === 0) return { pose: createEmptyMotionPose(), contributions: [] };
|
|
232
|
+
if (children.length === 1 || value <= children[0].threshold) weightedChildren = [{ child: children[0], weight: 1 }];
|
|
233
|
+
else if (value >= children.at(-1).threshold) weightedChildren = [{ child: children.at(-1), weight: 1 }];
|
|
234
|
+
else {
|
|
235
|
+
let upper = 1;
|
|
236
|
+
while (upper < children.length && children[upper].threshold < value) upper += 1;
|
|
237
|
+
const a = children[upper - 1];
|
|
238
|
+
const b = children[upper];
|
|
239
|
+
const t = (value - a.threshold) / Math.max(b.threshold - a.threshold, 1e-6);
|
|
240
|
+
weightedChildren = [{ child: a, weight: 1 - t }, { child: b, weight: t }];
|
|
241
|
+
}
|
|
242
|
+
} else if (node.type === 'blend2d') {
|
|
243
|
+
const x = finite(parameterValue(parameterValues, node.parameterX, 0), 0);
|
|
244
|
+
const y = finite(parameterValue(parameterValues, node.parameterY, 0), 0);
|
|
245
|
+
const ranked = (node.children ?? []).map((child) => ({
|
|
246
|
+
child,
|
|
247
|
+
distance: Math.hypot(x - child.x, y - child.y),
|
|
248
|
+
})).sort((a, b) => a.distance - b.distance);
|
|
249
|
+
if (ranked[0]?.distance < 1e-6) weightedChildren = [{ child: ranked[0].child, weight: 1 }];
|
|
250
|
+
else weightedChildren = addWeights(ranked.slice(0, 3).map((entry) => ({ child: entry.child, weight: 1 / Math.max(entry.distance ** 2, 1e-6) })));
|
|
251
|
+
} else {
|
|
252
|
+
weightedChildren = addWeights((node.children ?? []).map((child) => ({
|
|
253
|
+
child,
|
|
254
|
+
weight: child.weightParameter
|
|
255
|
+
? Math.max(0, finite(parameterValue(parameterValues, child.weightParameter, child.weight), child.weight))
|
|
256
|
+
: child.weight,
|
|
257
|
+
})));
|
|
258
|
+
}
|
|
259
|
+
const sampled = weightedChildren.map(({ child, weight }, index) => {
|
|
260
|
+
const result = sampleNode(child.node, time, `${prefix}.${index}`, inheritedWeight * weight);
|
|
261
|
+
return { ...result, weight };
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
pose: blendMotionPoseList(sampled.map((entry) => ({ pose: entry.pose, weight: entry.weight }))),
|
|
265
|
+
contributions: mergeContributions(sampled.flatMap((entry) => entry.contributions)),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function sampleState(stateId, time, prefix = stateId) {
|
|
270
|
+
const state = graph.states[stateId];
|
|
271
|
+
if (!state) return { pose: createEmptyMotionPose(), contributions: [], duration: 1 };
|
|
272
|
+
const result = sampleNode(state.node, time * state.speed, `${prefix}.base`);
|
|
273
|
+
let output = result.pose;
|
|
274
|
+
const contributions = [...result.contributions];
|
|
275
|
+
for (const layer of state.layers) {
|
|
276
|
+
if (layer.enabledParameter && !parameterValue(parameterValues, layer.enabledParameter, false)) continue;
|
|
277
|
+
const parameterWeight = layer.weightParameter
|
|
278
|
+
? finite(parameterValue(parameterValues, layer.weightParameter, layer.weight), layer.weight)
|
|
279
|
+
: layer.weight;
|
|
280
|
+
const weight = clamp01(parameterWeight);
|
|
281
|
+
if (weight <= 0) continue;
|
|
282
|
+
const sampled = sampleNode(layer.node, time * state.speed, `${prefix}.layer.${layer.id}`, weight);
|
|
283
|
+
output = layerMotionPose(output, sampled.pose, { mask: layer.mask, mode: layer.mode, weight });
|
|
284
|
+
contributions.push(...sampled.contributions);
|
|
285
|
+
}
|
|
286
|
+
const first = contributions[0];
|
|
287
|
+
return {
|
|
288
|
+
pose: output,
|
|
289
|
+
contributions: mergeContributions(contributions),
|
|
290
|
+
duration: first ? clipDuration(first.sampler) / Math.max(finite(first.slot.speed, 1) * state.speed, 1e-6) : 1,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function statePhase(stateId, time) {
|
|
295
|
+
const sampled = sampleState(stateId, time, `${stateId}.phase`);
|
|
296
|
+
return sampled.duration > 0 ? (time % sampled.duration) / sampled.duration : 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function canInterrupt() {
|
|
300
|
+
if (!transition) return true;
|
|
301
|
+
if (transition.interruptible !== null) return transition.interruptible;
|
|
302
|
+
return settings.transitions.allowInterrupt;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function findTransition() {
|
|
306
|
+
if (!canInterrupt()) return null;
|
|
307
|
+
const state = graph.states[currentState];
|
|
308
|
+
if (!state) return null;
|
|
309
|
+
const phase = statePhase(currentState, stateTime);
|
|
310
|
+
return state.transitions.find((edge) => (
|
|
311
|
+
transitionExitReady(edge, phase)
|
|
312
|
+
&& edge.conditions.every((condition) => evaluateMotionCondition(condition, parameterValues, triggers))
|
|
313
|
+
)) ?? null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function beginTransition(edge) {
|
|
317
|
+
if (!edge || !graph.states[edge.to] || edge.to === currentState) return false;
|
|
318
|
+
const fromState = currentState;
|
|
319
|
+
const fromTime = stateTime;
|
|
320
|
+
const shouldSync = edge.syncPhase ?? settings.transitions.syncPhase;
|
|
321
|
+
const sourcePhase = shouldSync ? statePhase(fromState, fromTime) : 0;
|
|
322
|
+
const targetDuration = sampleState(edge.to, 0, `${edge.to}.sync`).duration;
|
|
323
|
+
currentState = edge.to;
|
|
324
|
+
stateTime = sourcePhase * targetDuration;
|
|
325
|
+
transition = {
|
|
326
|
+
edgeId: edge.id,
|
|
327
|
+
fromState,
|
|
328
|
+
fromTime,
|
|
329
|
+
elapsed: 0,
|
|
330
|
+
duration: edge.duration >= 0 ? edge.duration : settings.transitions.duration,
|
|
331
|
+
interruptible: edge.interruptible,
|
|
332
|
+
};
|
|
333
|
+
eventCursors.clear();
|
|
334
|
+
rootCursors.clear();
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function dispatchEvents(contributions, delta) {
|
|
339
|
+
const emitted = [];
|
|
340
|
+
const maxEvents = settings.events.maxPerUpdate;
|
|
341
|
+
for (const contribution of contributions) {
|
|
342
|
+
if (emitted.length >= maxEvents || contribution.weight < settings.events.minimumWeight) continue;
|
|
343
|
+
const duration = clipDuration(contribution.sampler);
|
|
344
|
+
const events = [
|
|
345
|
+
...(Array.isArray(contribution.sampler.events) ? contribution.sampler.events : []),
|
|
346
|
+
...(Array.isArray(contribution.slot.events) ? contribution.slot.events : []),
|
|
347
|
+
];
|
|
348
|
+
const current = Math.max(0, contribution.rawTime);
|
|
349
|
+
const previous = eventCursors.has(contribution.key)
|
|
350
|
+
? eventCursors.get(contribution.key)
|
|
351
|
+
: Math.max(0, current - delta * Math.max(contribution.slot.speed, 0));
|
|
352
|
+
eventCursors.set(contribution.key, current);
|
|
353
|
+
if (current < previous || events.length === 0) continue;
|
|
354
|
+
const pendingEvents = events.map((event, index) => ({ event, index })).filter(({ event, index }) => (
|
|
355
|
+
!event.once || !firedOnce.has(`${contribution.key}:${eventIdentity(event, index)}`)
|
|
356
|
+
));
|
|
357
|
+
if (pendingEvents.length === 0) continue;
|
|
358
|
+
const startCycle = contribution.loop ? Math.floor(previous / duration) : 0;
|
|
359
|
+
const endCycle = contribution.loop ? Math.floor(current / duration) : 0;
|
|
360
|
+
const cycleLimit = settings.events.fireOnLoop ? endCycle : Math.min(endCycle, startCycle);
|
|
361
|
+
// A partial first cycle may contain no occurrence. Every later complete
|
|
362
|
+
// cycle contains every pending clip-local event, so visiting at most one
|
|
363
|
+
// more cycle than the remaining emission budget is sufficient. This
|
|
364
|
+
// keeps tiny, very fast looping clips bounded even across huge deltas.
|
|
365
|
+
const cycleBudget = Math.min(
|
|
366
|
+
Math.max(0, cycleLimit - startCycle + 1),
|
|
367
|
+
maxEvents - emitted.length + 1,
|
|
368
|
+
);
|
|
369
|
+
const visitedCycleLimit = startCycle + cycleBudget - 1;
|
|
370
|
+
for (let cycle = startCycle; cycle <= visitedCycleLimit && emitted.length < maxEvents; cycle += 1) {
|
|
371
|
+
for (const { event, index } of pendingEvents) {
|
|
372
|
+
if (emitted.length >= maxEvents) break;
|
|
373
|
+
const occurrence = eventTime(event, duration) + cycle * duration;
|
|
374
|
+
const inclusiveStart = previous === 0 && occurrence === 0;
|
|
375
|
+
if (!inclusiveStart && occurrence <= previous) continue;
|
|
376
|
+
if (occurrence > current || (!contribution.loop && occurrence > duration)) continue;
|
|
377
|
+
const onceKey = `${contribution.key}:${eventIdentity(event, index)}`;
|
|
378
|
+
if (event.once && firedOnce.has(onceKey)) continue;
|
|
379
|
+
if (event.once) firedOnce.add(onceKey);
|
|
380
|
+
emitted.push({
|
|
381
|
+
id: eventIdentity(event, index),
|
|
382
|
+
name: String(event.name ?? event.type ?? 'event'),
|
|
383
|
+
payload: plain(event.payload) ? { ...event.payload } : {},
|
|
384
|
+
slot: contribution.slotId,
|
|
385
|
+
state: currentState,
|
|
386
|
+
time: occurrence,
|
|
387
|
+
weight: contribution.weight,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const event of emitted) {
|
|
393
|
+
totalEventCount += 1;
|
|
394
|
+
for (const listener of [...eventListeners]) listener(event);
|
|
395
|
+
}
|
|
396
|
+
return emitted;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function extractRootMotion(contributions) {
|
|
400
|
+
const position = [0, 0, 0];
|
|
401
|
+
let yaw = 0;
|
|
402
|
+
for (const contribution of contributions) {
|
|
403
|
+
if (contribution.weight <= 0) continue;
|
|
404
|
+
const current = contribution.rawTime;
|
|
405
|
+
const previous = rootCursors.has(contribution.key) ? rootCursors.get(contribution.key) : current;
|
|
406
|
+
rootCursors.set(contribution.key, current);
|
|
407
|
+
const from = motionAtAbsoluteTime(contribution.sampler, previous, contribution.loop);
|
|
408
|
+
const to = motionAtAbsoluteTime(contribution.sampler, current, contribution.loop);
|
|
409
|
+
addScaledVector(position, subtractVector(to.position, from.position), contribution.weight);
|
|
410
|
+
yaw += (contribution.loop
|
|
411
|
+
? to.yaw - from.yaw
|
|
412
|
+
: shortestAngleDelta(from.yaw, to.yaw)) * contribution.weight;
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
position: position.map((value, index) => (
|
|
416
|
+
settings.rootMotion.axes[index] ? value * settings.rootMotion.scale : 0
|
|
417
|
+
)),
|
|
418
|
+
yaw,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function applyModifiers(inputPose, delta) {
|
|
423
|
+
const output = normalizeMotionPose(inputPose);
|
|
424
|
+
const leanTarget = settings.lean.enabled
|
|
425
|
+
? Math.min(Math.max(finite(parameterValue(parameterValues, settings.lean.turnParameter, 0)), -1), 1) * settings.lean.maxAngle
|
|
426
|
+
: 0;
|
|
427
|
+
leanValue += (leanTarget - leanValue) * (1 - Math.exp(-settings.lean.response * delta));
|
|
428
|
+
output.root.rotation[2] -= leanValue;
|
|
429
|
+
const speed = Math.abs(finite(parameterValue(parameterValues, settings.bob.speedParameter, 0), 0));
|
|
430
|
+
if (settings.bob.enabled && speed > 0.001) {
|
|
431
|
+
const phase = clock * Math.PI * 2 * settings.bob.frequency * Math.max(speed, 0.15);
|
|
432
|
+
output.root.position[1] += Math.abs(Math.sin(phase)) * settings.bob.amplitude * Math.min(speed, 1.5);
|
|
433
|
+
output.root.position[0] += Math.sin(phase * 0.5) * settings.bob.lateral * Math.min(speed, 1.5);
|
|
434
|
+
}
|
|
435
|
+
const vertical = Math.abs(finite(parameterValue(parameterValues, settings.squash.verticalParameter, 0), 0));
|
|
436
|
+
const squashTarget = settings.squash.enabled ? Math.min(vertical * settings.squash.amount * 0.12, settings.squash.amount) : 0;
|
|
437
|
+
squashValue += (squashTarget - squashValue) * (1 - Math.exp(-settings.squash.response * delta));
|
|
438
|
+
output.root.scale[1] *= 1 - squashValue;
|
|
439
|
+
output.root.scale[0] *= 1 + squashValue * 0.5;
|
|
440
|
+
output.root.scale[2] *= 1 + squashValue * 0.5;
|
|
441
|
+
return output;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function update(deltaSeconds = 0) {
|
|
445
|
+
if (disposed) return { events: [], pose, rootMotion: [0, 0, 0], state: currentState };
|
|
446
|
+
const started = now();
|
|
447
|
+
const unscaled = Math.min(Math.max(finite(deltaSeconds, 0), 0), settings.playback.maxDelta);
|
|
448
|
+
const delta = unscaled * settings.playback.speed;
|
|
449
|
+
clock += delta;
|
|
450
|
+
sampleCount = 0;
|
|
451
|
+
|
|
452
|
+
const edge = findTransition();
|
|
453
|
+
if (edge) beginTransition(edge);
|
|
454
|
+
stateTime += delta;
|
|
455
|
+
let sampled = sampleState(currentState, stateTime);
|
|
456
|
+
if (transition) {
|
|
457
|
+
transition.elapsed += delta;
|
|
458
|
+
transition.fromTime += delta;
|
|
459
|
+
const source = sampleState(transition.fromState, transition.fromTime, `${transition.fromState}.out`);
|
|
460
|
+
const amount = transition.duration <= 0 ? 1 : ease(transition.elapsed / transition.duration, settings.transitions.easing);
|
|
461
|
+
sampled = {
|
|
462
|
+
pose: blendMotionPoses(source.pose, sampled.pose, amount),
|
|
463
|
+
contributions: mergeContributions([
|
|
464
|
+
...source.contributions.map((entry) => ({ ...entry, weight: entry.weight * (1 - amount) })),
|
|
465
|
+
...sampled.contributions.map((entry) => ({ ...entry, weight: entry.weight * amount })),
|
|
466
|
+
]),
|
|
467
|
+
duration: sampled.duration,
|
|
468
|
+
};
|
|
469
|
+
if (amount >= 1) transition = null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const rootDelta = extractRootMotion(sampled.contributions);
|
|
473
|
+
const policy = settings.rootMotion.policy;
|
|
474
|
+
pose = applyModifiers(sampled.pose, delta);
|
|
475
|
+
if (policy === 'inPlace' || policy === 'extract' || policy === 'apply') pose.root.position = [0, 0, 0];
|
|
476
|
+
if ((policy === 'extract' || policy === 'apply') && settings.rootMotion.applyYaw) pose.root.rotation[1] = 0;
|
|
477
|
+
if (policy === 'apply') rigAdapter?.applyRootMotion?.(
|
|
478
|
+
rootDelta.position,
|
|
479
|
+
settings.rootMotion.applyYaw ? rootDelta.yaw : 0,
|
|
480
|
+
);
|
|
481
|
+
rigAdapter?.applyPose?.(pose);
|
|
482
|
+
const events = dispatchEvents(sampled.contributions, delta);
|
|
483
|
+
for (const [id, definition] of Object.entries(graph.parameters)) {
|
|
484
|
+
if (definition.type === 'trigger') parameterValues[id] = false;
|
|
485
|
+
}
|
|
486
|
+
triggers.clear();
|
|
487
|
+
updateCount += 1;
|
|
488
|
+
lastUpdateMs = now() - started;
|
|
489
|
+
return {
|
|
490
|
+
events,
|
|
491
|
+
pose,
|
|
492
|
+
rootMotion: policy === 'extract' || policy === 'apply' ? rootDelta.position : [0, 0, 0],
|
|
493
|
+
rootYaw: policy === 'extract' || policy === 'apply' ? rootDelta.yaw : 0,
|
|
494
|
+
state: currentState,
|
|
495
|
+
transitioning: Boolean(transition),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function reset({ state = graph.initial, keepParameters = false } = {}) {
|
|
500
|
+
currentState = graph.states[state] ? state : graph.initial;
|
|
501
|
+
stateTime = 0;
|
|
502
|
+
clock = 0;
|
|
503
|
+
transition = null;
|
|
504
|
+
pose = createEmptyMotionPose();
|
|
505
|
+
leanValue = 0;
|
|
506
|
+
squashValue = 0;
|
|
507
|
+
triggers.clear();
|
|
508
|
+
eventCursors.clear();
|
|
509
|
+
rootCursors.clear();
|
|
510
|
+
firedOnce.clear();
|
|
511
|
+
missingSlots.clear();
|
|
512
|
+
if (!keepParameters) resetParameters();
|
|
513
|
+
rigAdapter?.reset?.();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const api = {
|
|
517
|
+
update,
|
|
518
|
+
reset,
|
|
519
|
+
setParameter(id, value) {
|
|
520
|
+
if (disposed) return api;
|
|
521
|
+
const definition = graph.parameters[id];
|
|
522
|
+
if (definition?.type === 'boolean') parameterValues[id] = Boolean(value);
|
|
523
|
+
else if (definition?.type === 'trigger') {
|
|
524
|
+
if (value) triggers.add(id);
|
|
525
|
+
parameterValues[id] = Boolean(value);
|
|
526
|
+
} else parameterValues[id] = finite(value, value);
|
|
527
|
+
return api;
|
|
528
|
+
},
|
|
529
|
+
setParameters(values = {}) {
|
|
530
|
+
for (const [id, value] of Object.entries(values)) api.setParameter(id, value);
|
|
531
|
+
return api;
|
|
532
|
+
},
|
|
533
|
+
trigger(id) {
|
|
534
|
+
triggers.add(String(id));
|
|
535
|
+
parameterValues[id] = true;
|
|
536
|
+
return api;
|
|
537
|
+
},
|
|
538
|
+
transitionTo(state, options = {}) {
|
|
539
|
+
if (!graph.states[state]) return false;
|
|
540
|
+
return beginTransition({
|
|
541
|
+
id: options.id ?? 'manual',
|
|
542
|
+
to: state,
|
|
543
|
+
duration: Math.max(0, finite(options.duration, settings.transitions.duration)),
|
|
544
|
+
interruptible: options.interruptible ?? null,
|
|
545
|
+
syncPhase: options.syncPhase ?? false,
|
|
546
|
+
});
|
|
547
|
+
},
|
|
548
|
+
setSettings(value) {
|
|
549
|
+
settings = createMotionSettings(value);
|
|
550
|
+
return api;
|
|
551
|
+
},
|
|
552
|
+
setGraph(value, { preserveState = true } = {}) {
|
|
553
|
+
const result = validateMotionGraph(value, { clipSlots });
|
|
554
|
+
if (!result.ok) throw new Error(result.errors.join(' '));
|
|
555
|
+
const previousState = currentState;
|
|
556
|
+
graph = result.value;
|
|
557
|
+
resetParameters(parameterValues);
|
|
558
|
+
reset({ state: preserveState && graph.states[previousState] ? previousState : graph.initial, keepParameters: true });
|
|
559
|
+
return api;
|
|
560
|
+
},
|
|
561
|
+
setClipSlots(value) {
|
|
562
|
+
clipSlots = createMotionClipSlots(value);
|
|
563
|
+
missingSlots.clear();
|
|
564
|
+
eventCursors.clear();
|
|
565
|
+
rootCursors.clear();
|
|
566
|
+
return api;
|
|
567
|
+
},
|
|
568
|
+
setClips(value, resolver = clipResolver) {
|
|
569
|
+
clipLibrary = value ?? {};
|
|
570
|
+
clipResolver = typeof resolver === 'function' ? resolver : null;
|
|
571
|
+
missingSlots.clear();
|
|
572
|
+
return api;
|
|
573
|
+
},
|
|
574
|
+
setRig(value) {
|
|
575
|
+
rigAdapter = value;
|
|
576
|
+
return api;
|
|
577
|
+
},
|
|
578
|
+
onEvent(listener) {
|
|
579
|
+
eventListeners.add(listener);
|
|
580
|
+
return () => eventListeners.delete(listener);
|
|
581
|
+
},
|
|
582
|
+
getState() {
|
|
583
|
+
return {
|
|
584
|
+
state: currentState,
|
|
585
|
+
stateTime,
|
|
586
|
+
transition: transition ? { ...transition } : null,
|
|
587
|
+
parameters: { ...parameterValues },
|
|
588
|
+
pose: normalizeMotionPose(pose),
|
|
589
|
+
};
|
|
590
|
+
},
|
|
591
|
+
stats() {
|
|
592
|
+
const rigStats = rigAdapter?.stats?.() ?? {};
|
|
593
|
+
const slotIds = Object.keys(clipSlots);
|
|
594
|
+
const missingBoundSlots = slotIds.filter((id) => missingSlots.has(id)).length;
|
|
595
|
+
return {
|
|
596
|
+
activeState: currentState,
|
|
597
|
+
boundClipCount: slotIds.length - missingBoundSlots,
|
|
598
|
+
clipSlotCount: slotIds.length,
|
|
599
|
+
disposed,
|
|
600
|
+
eventCount: totalEventCount,
|
|
601
|
+
lastUpdateMs,
|
|
602
|
+
missingBoneCount: rigStats.missingBoneCount ?? 0,
|
|
603
|
+
missingBones: rigStats.missingBones ?? [],
|
|
604
|
+
missingSlotCount: missingSlots.size,
|
|
605
|
+
missingSlots: [...missingSlots],
|
|
606
|
+
sampleCount,
|
|
607
|
+
stateCount: Object.keys(graph.states).length,
|
|
608
|
+
transition: transition?.edgeId ?? null,
|
|
609
|
+
updateCount,
|
|
610
|
+
};
|
|
611
|
+
},
|
|
612
|
+
dispose({ disposeClips = false, disposeRig = false } = {}) {
|
|
613
|
+
if (disposed) return;
|
|
614
|
+
disposed = true;
|
|
615
|
+
eventListeners.clear();
|
|
616
|
+
eventCursors.clear();
|
|
617
|
+
rootCursors.clear();
|
|
618
|
+
if (disposeClips) {
|
|
619
|
+
const values = clipLibrary instanceof Map ? [...clipLibrary.values()] : Object.values(plain(clipLibrary) ? clipLibrary : {});
|
|
620
|
+
for (const sampler of new Set(values)) sampler?.dispose?.();
|
|
621
|
+
}
|
|
622
|
+
if (disposeRig) rigAdapter?.dispose?.();
|
|
623
|
+
clipLibrary = {};
|
|
624
|
+
rigAdapter = null;
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
return api;
|
|
628
|
+
}
|