@volter/editor-blender 0.1.150 → 0.1.152
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/LICENSE +18 -9
- package/README.md +5 -5
- package/contributions/blender-animation-editors.tsx +17 -69
- package/contributions/blender-display-settings.ts +36 -0
- package/contributions/blender-export-gltf.ts +228 -0
- package/contributions/blender-export.verbs.ts +39 -0
- package/contributions/blender-gltf-export.verbs.ts +312 -0
- package/contributions/blender-header-menus.tsx +37 -133
- package/contributions/blender-icon-trace.mjs +8 -98
- package/contributions/blender-icons.source.mjs +96 -860
- package/contributions/blender-mixer-pose.ts +369 -0
- package/contributions/blender-model-opening.tsx +26 -56
- package/contributions/blender-node-editor.document.tsx +30 -181
- package/contributions/blender-node-geometry.ts +44 -230
- package/contributions/blender-node-panels.source.mjs +7 -121
- package/contributions/blender-outliner-authoring.ts +61 -415
- package/contributions/blender-outliner-model.ts +15 -112
- package/contributions/blender-play-area.tsx +99 -0
- package/contributions/blender-playback-movie.ts +409 -0
- package/contributions/blender-pose.ts +18 -368
- package/contributions/blender-properties-context.ts +1 -1
- package/contributions/blender-properties-model.ts +59 -91
- package/contributions/blender-properties-tab.tsx +3 -50
- package/contributions/blender-properties-view.tsx +39 -181
- package/contributions/blender-remedy.ts +14 -0
- package/contributions/blender-runtime-skin.ts +103 -99
- package/contributions/blender-runtime.document.tsx +170 -618
- package/contributions/blender-scene-mixer.ts +51 -0
- package/contributions/blender-screenshot.service.ts +28 -0
- package/contributions/blender-timeline-geometry.ts +16 -136
- package/contributions/blender-timeline.document.tsx +145 -333
- package/contributions/blender-uv-editor.document.tsx +48 -112
- package/contributions/blender-uv-geometry.ts +14 -179
- package/contributions/blender-version.status.tsx +45 -31
- package/contributions/blender.environment.ts +2 -2
- package/contributions/blender.icons.json +0 -9
- package/contributions/blender.keymap.ts +9 -51
- package/contributions/blender.node-panels.json +4 -13
- package/contributions/blender.page.ts +64 -0
- package/contributions/blender.status.tsx +8 -150
- package/contributions/blender.style.ts +25 -152
- package/contributions/blender.verbs.ts +52 -0
- package/contributions/model.layout.ts +2 -34
- package/contributions/models.finder.ts +3 -30
- package/contributions/properties-bone-constraints.inspector.tsx +1 -16
- package/contributions/properties-bone.inspector.tsx +2 -31
- package/contributions/properties-collection.inspector.tsx +1 -34
- package/contributions/properties-constraints.inspector.tsx +1 -21
- package/contributions/properties-data.inspector.tsx +2 -39
- package/contributions/properties-material.inspector.tsx +2 -33
- package/contributions/properties-modifiers.inspector.tsx +2 -30
- package/contributions/properties-object.inspector.tsx +2 -44
- package/contributions/properties-output.inspector.tsx +1 -27
- package/contributions/properties-particles.inspector.tsx +2 -46
- package/contributions/properties-physics.inspector.tsx +2 -41
- package/contributions/properties-render.inspector.tsx +1 -45
- package/contributions/properties-scene.inspector.tsx +1 -35
- package/contributions/properties-texture.inspector.tsx +1 -49
- package/contributions/properties-view-layer.inspector.tsx +1 -29
- package/contributions/properties-world.inspector.tsx +2 -39
- package/contributions/shading.layout.ts +3 -78
- package/contributions/uv-editing.layout.ts +3 -70
- package/dist-node/serving.mjs +13 -32
- package/host/blender-runtime-host.ts +795 -412
- package/host/scene-sound.ts +143 -0
- package/node/blender-mcp.ts +52 -95
- package/package.json +1221 -46
- package/producticons/blender-icons.otf +0 -0
- package/producticons/blender-product-icon-theme.json +129 -0
- package/serving/blender-routes.ts +9 -48
- package/serving/blender-wasm-artifact.ts +29 -166
- package/serving/index.ts +1 -1
- package/src/api.ts +9 -0
- package/src/layouts.tsx +1 -17
- package/src/looks.ts +1 -12
- package/src/model-document-saving.ts +25 -0
- package/src/node-view-state.ts +3 -25
- package/src/play-mode.ts +13 -42
- package/src/play-tool.ts +44 -0
- package/src/presentation.ts +6 -18
- package/src/regions.ts +3 -43
- package/src/start-document.ts +15 -0
- package/src/timeline-view-state.ts +4 -39
- package/src/uv-view-state.ts +4 -20
- package/themes/blender.json +299 -0
- package/view/blender-view-routes.ts +1 -1
- package/contributions/blender-game-panel.tsx +0 -809
- package/contributions/blender-palette.source.mjs +0 -350
- package/contributions/blender-play-skin.ts +0 -508
- package/contributions/blender-screenshot.menu.ts +0 -32
- package/contributions/blender.command.ts +0 -109
- package/contributions/blender.palette.json +0 -102
- package/src/play-live.ts +0 -27
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* THE NLA'S TIMING: which strip of each track a scene frame falls on, the action frame that strip
|
|
3
|
+
* shows, its influence, and where the active action is placed over the tracks, for the Timeline
|
|
4
|
+
* (`blender-runtime-skin.ts`), following what Blender itself does:
|
|
5
5
|
*
|
|
6
6
|
* - THE STACK is `BKE_animsys_evaluate_animdata`'s: the NLA tracks bottom to top (mute and solo as
|
|
7
7
|
* Blender reads them), then the active action over them. A strip's time is
|
|
8
8
|
* `nlastrip_get_frame_actionclip`'s (scale, repeat, reverse), its influence its blend in and out
|
|
9
|
-
* or its
|
|
10
|
-
*
|
|
11
|
-
* Combine, Add, Subtract or Multiply at the layer's influence. A component no layer animates
|
|
12
|
-
* keeps the bone's own value; one that is animated starts from the property's default. So an
|
|
13
|
-
* action that keys only the upper body layers over one that keys the legs, with no masks.
|
|
14
|
-
* - THE MATRIX is `BKE_pchan_to_mat4` then `BKE_armature_mat_bone_to_pose`: rotation by the
|
|
15
|
-
* bone's own mode, and `parent @ rest @ basis` down the hierarchy.
|
|
16
|
-
* - CONSTRAINTS run per bone after its parent, in stack order, blended by their influence as
|
|
17
|
-
* `BKE_constraints_solve` blends them. Damped Track is evaluated (`damptrack_do_transform`).
|
|
18
|
-
* Any other type is not, and is named (`unsupported`): its effect is Blender's alone.
|
|
9
|
+
* or its keyed `influence` (that one F-Curve evaluated as `fcurve_eval_keyframes` does), and
|
|
10
|
+
* outside it the track holds as its extrapolation says.
|
|
19
11
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `fcm_cycles_time` evaluate them, so a pose is Blender's at any frame, whole or fractional.
|
|
12
|
+
* The layers it answers are played by three.js (`blender-mixer-pose.ts`): Blender samples each
|
|
13
|
+
* action, and an `AnimationMixer` blends them.
|
|
23
14
|
*
|
|
24
15
|
* LICENCE. This file follows Blender's source (the functions named above), so it is a derivative
|
|
25
16
|
* work of Blender and is GPL-3.0-or-later, not the AGPL the package's own code carries
|
|
@@ -28,31 +19,13 @@
|
|
|
28
19
|
*
|
|
29
20
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
30
21
|
*/
|
|
31
|
-
import
|
|
32
|
-
import type {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
BlenderArmatureBone,
|
|
36
|
-
BlenderNlaStrip,
|
|
37
|
-
} from '@volter/blender-engine/browser/three/blender-runtime-armature';
|
|
38
|
-
import type { ArmatureRig } from '@volter/blender-engine/browser/three/blender-runtime-skeleton';
|
|
39
|
-
import * as THREE from 'three';
|
|
40
|
-
|
|
41
|
-
type Channel = 'location' | 'rotation_quaternion' | 'rotation_euler' | 'rotation_axis_angle' | 'scale';
|
|
42
|
-
const CHANNELS: readonly Channel[] = ['location', 'rotation_quaternion', 'rotation_euler', 'rotation_axis_angle', 'scale'];
|
|
43
|
-
const DEFAULTS: Record<Channel, readonly number[]> = {
|
|
44
|
-
location: [0, 0, 0],
|
|
45
|
-
rotation_quaternion: [1, 0, 0, 0],
|
|
46
|
-
rotation_euler: [0, 0, 0],
|
|
47
|
-
// Blender's NLA default for an axis-angle channel is all zeros (`anim_sys.cc`, zero_v4).
|
|
48
|
-
rotation_axis_angle: [0, 0, 0, 0],
|
|
49
|
-
scale: [1, 1, 1],
|
|
50
|
-
};
|
|
22
|
+
import { bytesFromBase64 } from '@volter/blender-engine/browser/three/blender-base64';
|
|
23
|
+
import type { BlenderClipCurve } from '@volter/blender-engine/browser/rna';
|
|
24
|
+
import type { BlenderArmatureAnimation, BlenderNlaStrip } from '@volter/blender-engine/browser/three/blender-runtime-armature';
|
|
25
|
+
import type { MixerClip, MixerLayer } from './blender-mixer-pose';
|
|
51
26
|
|
|
52
27
|
function float32Of(base64: string): Float32Array {
|
|
53
|
-
const
|
|
54
|
-
const bytes = new Uint8Array(binary.length);
|
|
55
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
28
|
+
const bytes = bytesFromBase64(base64);
|
|
56
29
|
return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 2);
|
|
57
30
|
}
|
|
58
31
|
|
|
@@ -65,49 +38,6 @@ interface PoseCurve {
|
|
|
65
38
|
readonly cycles: readonly [number, number, number, number] | null;
|
|
66
39
|
}
|
|
67
40
|
|
|
68
|
-
/** One action read for one armature: its F-Curves, by bone and channel, one per component. */
|
|
69
|
-
export interface PoseClip {
|
|
70
|
-
readonly action: string;
|
|
71
|
-
readonly start: number;
|
|
72
|
-
readonly end: number;
|
|
73
|
-
/** Where Blender holds the active action over NLA strips; infinite where Cycles repeats it. */
|
|
74
|
-
readonly keysStart: number;
|
|
75
|
-
readonly keysEnd: number;
|
|
76
|
-
readonly cyclic: boolean;
|
|
77
|
-
readonly channels: readonly {
|
|
78
|
-
readonly bone: string;
|
|
79
|
-
readonly channel: Channel;
|
|
80
|
-
readonly mask: number;
|
|
81
|
-
readonly stride: number;
|
|
82
|
-
readonly curves: readonly (PoseCurve | null)[];
|
|
83
|
-
}[];
|
|
84
|
-
/** What its curves do in Blender that is not played. */
|
|
85
|
-
readonly unsupported: readonly string[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The clip door's curves as a clip, or null when it holds nothing a pose can take. */
|
|
89
|
-
export function poseClip(clip: BlenderActionClip): PoseClip | null {
|
|
90
|
-
if (!clip.action || clip.clipStart === undefined || clip.clipEnd === undefined || !clip.tracks.length) return null;
|
|
91
|
-
const channels = new Map<string, { bone: string; channel: Channel; mask: number; stride: number; curves: (PoseCurve | null)[] }>();
|
|
92
|
-
const unsupported = new Set<string>();
|
|
93
|
-
for (const track of clip.tracks) {
|
|
94
|
-
const key = `${track.bone}\u0000${track.property}`;
|
|
95
|
-
const stride = DEFAULTS[track.property].length;
|
|
96
|
-
let entry = channels.get(key);
|
|
97
|
-
if (!entry) channels.set(key, entry = { bone: track.bone, channel: track.property, mask: 0, stride, curves: Array.from({ length: stride }, () => null) });
|
|
98
|
-
if (track.index < 0 || track.index >= stride) continue;
|
|
99
|
-
entry.mask |= 1 << track.index;
|
|
100
|
-
entry.curves[track.index] = poseCurve(track);
|
|
101
|
-
for (const thing of track.unsupported ?? []) unsupported.add(`${clip.action}'s ${thing}`);
|
|
102
|
-
}
|
|
103
|
-
for (const thing of clip.unsupported ?? []) unsupported.add(`${clip.action}'s ${thing}`);
|
|
104
|
-
return {
|
|
105
|
-
action: clip.action, start: clip.clipStart, end: clip.clipEnd,
|
|
106
|
-
keysStart: clip.keysStart === null ? -Infinity : clip.keysStart ?? clip.clipStart,
|
|
107
|
-
keysEnd: clip.keysEnd === null ? Infinity : clip.keysEnd ?? clip.clipEnd,
|
|
108
|
-
cyclic: clip.cyclic ?? false, channels: [...channels.values()], unsupported: [...unsupported] };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
41
|
function poseCurve(curve: Pick<BlenderClipCurve, 'extrapolation' | 'interpolation' | 'keysBase64'> & { readonly cycles?: BlenderClipCurve['cycles'] | undefined }): PoseCurve {
|
|
112
42
|
return { extrapolation: curve.extrapolation, interpolation: curve.interpolation, keys: float32Of(curve.keysBase64), cycles: curve.cycles ?? null };
|
|
113
43
|
}
|
|
@@ -246,14 +176,13 @@ export function curveAt(curve: PoseCurve, frame: number): number {
|
|
|
246
176
|
return keyframesAt(curve, time) + offset;
|
|
247
177
|
}
|
|
248
178
|
|
|
249
|
-
|
|
250
179
|
/**
|
|
251
180
|
* THE ACTIVE ACTION'S LAYER at a scene frame, placed as `animsys_calculate_nla` places it: a soloed
|
|
252
181
|
* track leaves it out; when no enabled track has strips (or the NLA is off) it is evaluated alone,
|
|
253
182
|
* whole, at full influence; otherwise it is one more strip spanning its own range, at its influence
|
|
254
183
|
* and blend type, holding or not past that range as its extrapolation says.
|
|
255
184
|
*/
|
|
256
|
-
export function actionLayer(animation: BlenderArmatureAnimation | undefined, clip:
|
|
185
|
+
export function actionLayer(animation: BlenderArmatureAnimation | undefined, clip: MixerClip, frame: number,
|
|
257
186
|
stripsEvaluated: boolean): PoseLayer | null {
|
|
258
187
|
const nla = animation?.useNla ?? true;
|
|
259
188
|
if (nla && animation!.tracks.some((track) => track.solo)) return null;
|
|
@@ -271,15 +200,10 @@ export function actionLayer(animation: BlenderArmatureAnimation | undefined, cli
|
|
|
271
200
|
}
|
|
272
201
|
|
|
273
202
|
/** One layer of the stack: an action at a frame of its own, an influence, a blend type. */
|
|
274
|
-
export
|
|
275
|
-
readonly clip: PoseClip;
|
|
276
|
-
readonly frame: number;
|
|
277
|
-
readonly influence: number;
|
|
278
|
-
readonly blend: string;
|
|
279
|
-
}
|
|
203
|
+
export type PoseLayer = MixerLayer;
|
|
280
204
|
|
|
281
205
|
/** A clip by action name: the clip, `undefined` while it is being baked, null when it has none. */
|
|
282
|
-
export type ClipSource = (action: string) =>
|
|
206
|
+
export type ClipSource = (action: string) => MixerClip | null | undefined;
|
|
283
207
|
|
|
284
208
|
/** The strip a track plays at `frame`, and the time it plays it at (`nlastrips_ctime_get_strip`). */
|
|
285
209
|
function stripAt(strips: readonly BlenderNlaStrip[], frame: number): { strip: BlenderNlaStrip; time: number } | null {
|
|
@@ -326,8 +250,7 @@ function stripInfluence(strip: BlenderNlaStrip, time: number): number {
|
|
|
326
250
|
* clip it needs is still being baked; `skipped` names what plays in Blender and not here;
|
|
327
251
|
* `evaluated` says an enabled track has strips (what places the active action over the NLA).
|
|
328
252
|
*/
|
|
329
|
-
export function nlaLayers(animation: BlenderArmatureAnimation | undefined, frame: number, clips: ClipSource
|
|
330
|
-
tracks?: (name: string) => boolean): { layers: PoseLayer[]; waiting: boolean; skipped: string[]; evaluated: boolean } {
|
|
253
|
+
export function nlaLayers(animation: BlenderArmatureAnimation | undefined, frame: number, clips: ClipSource): { layers: PoseLayer[]; waiting: boolean; skipped: string[]; evaluated: boolean } {
|
|
331
254
|
const layers: PoseLayer[] = [];
|
|
332
255
|
const skipped: string[] = [];
|
|
333
256
|
let waiting = false;
|
|
@@ -336,9 +259,8 @@ export function nlaLayers(animation: BlenderArmatureAnimation | undefined, frame
|
|
|
336
259
|
const solo = animation.tracks.some((track) => track.solo);
|
|
337
260
|
for (const track of animation.tracks) {
|
|
338
261
|
// ENABLED as `BKE_nlatrack_is_enabled` says: under a solo only the soloed track, muted or not.
|
|
339
|
-
if (
|
|
340
|
-
//
|
|
341
|
-
// falls on one of them (`animsys_evaluate_nla_for_flush`'s `has_strips`; measured in 5.2).
|
|
262
|
+
if (solo ? !track.solo : track.mute) continue;
|
|
263
|
+
// An enabled NLA track with strips places the active action over the NLA even when the current frame lies outside its strips (animsys_evaluate_nla_for_flush).
|
|
342
264
|
if (track.strips.length) evaluated = true;
|
|
343
265
|
const hit = stripAt(track.strips, frame);
|
|
344
266
|
if (!hit) continue;
|
|
@@ -354,275 +276,3 @@ export function nlaLayers(animation: BlenderArmatureAnimation | undefined, frame
|
|
|
354
276
|
}
|
|
355
277
|
return { layers, waiting, skipped, evaluated };
|
|
356
278
|
}
|
|
357
|
-
|
|
358
|
-
type Values = Record<Channel, number[]>;
|
|
359
|
-
|
|
360
|
-
const quatMul = (a: readonly number[], b: readonly number[]): number[] => [
|
|
361
|
-
a[0]! * b[0]! - a[1]! * b[1]! - a[2]! * b[2]! - a[3]! * b[3]!,
|
|
362
|
-
a[0]! * b[1]! + a[1]! * b[0]! + a[2]! * b[3]! - a[3]! * b[2]!,
|
|
363
|
-
a[0]! * b[2]! + a[2]! * b[0]! + a[3]! * b[1]! - a[1]! * b[3]!,
|
|
364
|
-
a[0]! * b[3]! + a[3]! * b[0]! + a[1]! * b[2]! - a[2]! * b[1]!,
|
|
365
|
-
];
|
|
366
|
-
const quatNormal = (q: readonly number[]): number[] => {
|
|
367
|
-
const length = Math.hypot(q[0]!, q[1]!, q[2]!, q[3]!);
|
|
368
|
-
return length > 0 ? q.map((v) => v / length) : [1, 0, 0, 0];
|
|
369
|
-
};
|
|
370
|
-
/** `pow_qt_fl_normalized`. */
|
|
371
|
-
const quatPow = (q: readonly number[], k: number): number[] => {
|
|
372
|
-
const angle = k * Math.acos(Math.max(-1, Math.min(1, q[0]!)));
|
|
373
|
-
const length = Math.hypot(q[1]!, q[2]!, q[3]!);
|
|
374
|
-
const s = Math.sin(angle);
|
|
375
|
-
return length > 0 ? [Math.cos(angle), (q[1]! / length) * s, (q[2]! / length) * s, (q[3]! / length) * s] : [Math.cos(angle), 0, 0, 0];
|
|
376
|
-
};
|
|
377
|
-
/** One layer blended into the values (`nla_blend_value`, `nla_combine_value`, `nla_combine_quaternion`). */
|
|
378
|
-
function blendLayer(values: Map<string, Values>, touched: Map<string, Record<Channel, number>>, layer: PoseLayer): void {
|
|
379
|
-
const k = Math.max(0, Math.min(1, layer.influence));
|
|
380
|
-
if (k === 0) return;
|
|
381
|
-
const frame = layer.frame;
|
|
382
|
-
for (const channel of layer.clip.channels) {
|
|
383
|
-
const bone = values.get(channel.bone);
|
|
384
|
-
if (!bone) continue;
|
|
385
|
-
const marks = touched.get(channel.bone)!;
|
|
386
|
-
const out = bone[channel.channel];
|
|
387
|
-
// A COMPONENT AN ACTION ANIMATES starts from the property's default, as Blender's NLA does.
|
|
388
|
-
for (let i = 0; i < channel.stride; i++) if (channel.mask & (1 << i) && !(marks[channel.channel] & (1 << i))) out[i] = DEFAULTS[channel.channel][i]!;
|
|
389
|
-
marks[channel.channel] |= channel.mask;
|
|
390
|
-
const sample = channel.curves.map((curve) => (curve ? curveAt(curve, frame) : 0));
|
|
391
|
-
if (layer.blend === 'COMBINE' && channel.channel === 'rotation_quaternion') {
|
|
392
|
-
const result = quatMul(quatNormal(out), quatPow(quatNormal(sample), k));
|
|
393
|
-
for (let i = 0; i < channel.stride; i++) if (channel.mask & (1 << i)) out[i] = result[i]!;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
for (let i = 0; i < channel.stride; i++) {
|
|
397
|
-
if (!(channel.mask & (1 << i))) continue;
|
|
398
|
-
const lower = out[i]!;
|
|
399
|
-
const upper = sample[i]!;
|
|
400
|
-
switch (layer.blend) {
|
|
401
|
-
case 'ADD': out[i] = lower + upper * k; break;
|
|
402
|
-
case 'SUBTRACT': out[i] = lower - upper * k; break;
|
|
403
|
-
case 'MULTIPLY': out[i] = k * (lower * upper) + (1 - k) * lower; break;
|
|
404
|
-
case 'COMBINE':
|
|
405
|
-
out[i] = channel.channel === 'scale' ? lower * Math.pow(upper, k) : lower + (upper - DEFAULTS[channel.channel][i]!) * k;
|
|
406
|
-
break;
|
|
407
|
-
default: out[i] = lower * (1 - k) + upper * k;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** `BKE_pchan_to_mat4`: the channels as the bone's basis matrix, rotation by its own mode. */
|
|
414
|
-
function basisMatrix(mode: string, values: Values): THREE.Matrix4 {
|
|
415
|
-
const rotation = new THREE.Quaternion();
|
|
416
|
-
if (mode === 'QUATERNION') {
|
|
417
|
-
const q = quatNormal(values.rotation_quaternion);
|
|
418
|
-
rotation.set(q[1]!, q[2]!, q[3]!, q[0]!);
|
|
419
|
-
} else if (mode === 'AXIS_ANGLE') {
|
|
420
|
-
const v = values.rotation_axis_angle;
|
|
421
|
-
rotation.setFromAxisAngle(new THREE.Vector3(v[1], v[2], v[3]).normalize(), v[0]!);
|
|
422
|
-
} else {
|
|
423
|
-
// Blender's `XYZ` applies X first; three's Euler order names the matrix product, so it reads
|
|
424
|
-
// the same rotation reversed.
|
|
425
|
-
const e = values.rotation_euler;
|
|
426
|
-
rotation.setFromEuler(new THREE.Euler(e[0], e[1], e[2], mode.split('').reverse().join('') as THREE.EulerOrder));
|
|
427
|
-
}
|
|
428
|
-
const [x, y, z] = values.location;
|
|
429
|
-
const [sx, sy, sz] = values.scale;
|
|
430
|
-
return new THREE.Matrix4().compose(new THREE.Vector3(x, y, z), rotation, new THREE.Vector3(sx, sy, sz));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const TRACK_AXES = ['TRACK_X', 'TRACK_Y', 'TRACK_Z', 'TRACK_NEGATIVE_X', 'TRACK_NEGATIVE_Y', 'TRACK_NEGATIVE_Z'];
|
|
434
|
-
const TRACK_VECTORS = [[1, 0, 0], [0, 1, 0], [0, 0, 1], [-1, 0, 0], [0, -1, 0], [0, 0, -1]] as const;
|
|
435
|
-
|
|
436
|
-
/** `damptrack_do_transform`: the smallest rotation that points the owner's axis at the target. */
|
|
437
|
-
function dampedTrack(owner: THREE.Matrix4, target: THREE.Vector3, axis: string): THREE.Matrix4 {
|
|
438
|
-
const flag = Math.max(0, TRACK_AXES.indexOf(axis));
|
|
439
|
-
const basis = new THREE.Matrix3().setFromMatrix4(owner);
|
|
440
|
-
const obvec = new THREE.Vector3(...TRACK_VECTORS[flag]!).applyMatrix3(basis);
|
|
441
|
-
if (obvec.lengthSq() === 0) return owner.clone();
|
|
442
|
-
obvec.normalize();
|
|
443
|
-
const obloc = new THREE.Vector3().setFromMatrixPosition(owner);
|
|
444
|
-
const tarvec = target.clone().sub(obloc);
|
|
445
|
-
if (tarvec.lengthSq() === 0) tarvec.copy(obvec);
|
|
446
|
-
else tarvec.normalize();
|
|
447
|
-
const raxis = new THREE.Vector3().crossVectors(obvec, tarvec);
|
|
448
|
-
let rangle = Math.acos(Math.max(-1, Math.min(1, obvec.dot(tarvec))));
|
|
449
|
-
const norm = raxis.length();
|
|
450
|
-
if (norm < 1.1920929e-7) {
|
|
451
|
-
if (Math.abs(rangle) < Math.PI - 0.01) return owner.clone();
|
|
452
|
-
rangle = Math.PI;
|
|
453
|
-
const other = new THREE.Vector3(...TRACK_VECTORS[(flag + 1) % 6]!).applyMatrix3(basis);
|
|
454
|
-
raxis.crossVectors(obvec, other);
|
|
455
|
-
if (raxis.lengthSq() === 0) return owner.clone();
|
|
456
|
-
} else if (norm < 0.1) {
|
|
457
|
-
rangle = rangle > Math.PI / 2 ? Math.PI - Math.asin(norm) : Math.asin(norm);
|
|
458
|
-
}
|
|
459
|
-
const turned = new THREE.Matrix4().makeRotationAxis(raxis.normalize(), rangle).multiply(owner);
|
|
460
|
-
return turned.setPosition(obloc);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/** `interp_m4_m4m4`: a constraint's result taken by its influence. */
|
|
464
|
-
function blendMatrix(before: THREE.Matrix4, after: THREE.Matrix4, k: number): THREE.Matrix4 {
|
|
465
|
-
if (k >= 1) return after;
|
|
466
|
-
const [p0, q0, s0] = [new THREE.Vector3(), new THREE.Quaternion(), new THREE.Vector3()];
|
|
467
|
-
const [p1, q1, s1] = [new THREE.Vector3(), new THREE.Quaternion(), new THREE.Vector3()];
|
|
468
|
-
before.decompose(p0, q0, s0);
|
|
469
|
-
after.decompose(p1, q1, s1);
|
|
470
|
-
return new THREE.Matrix4().compose(p0.lerp(p1, k), q0.slerp(q1, k), s0.lerp(s1, k));
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/** What a game changes on a constraint while it runs. */
|
|
474
|
-
export interface ConstraintOverride {
|
|
475
|
-
readonly influence?: number;
|
|
476
|
-
/** The object it aims at instead of the file's target. */
|
|
477
|
-
readonly target?: THREE.Object3D | null;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/** Where a constraint's targets are, in the scene the bones are in. */
|
|
481
|
-
export interface ConstraintWorld {
|
|
482
|
-
/** An object by its Blender name (its presented copy). */
|
|
483
|
-
object(name: string): THREE.Object3D | null;
|
|
484
|
-
/** A game's change to one constraint of one bone, if it made one. */
|
|
485
|
-
override?(bone: string, constraint: string): ConstraintOverride | undefined;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** One armature's bones, posed by the stack: Blender's evaluation, on the presenter's skeleton. */
|
|
489
|
-
export class ArmaturePose {
|
|
490
|
-
#bones: readonly BlenderArmatureBone[] = [];
|
|
491
|
-
#restRelative = new Map<string, THREE.Matrix4>();
|
|
492
|
-
#armature: BlenderArmature | null = null;
|
|
493
|
-
|
|
494
|
-
constructor(readonly rig: ArmatureRig) {}
|
|
495
|
-
|
|
496
|
-
/** Take the frame's facts for this armature (its bones' rest, channels and constraints). */
|
|
497
|
-
facts(armature: BlenderArmature): void {
|
|
498
|
-
if (armature === this.#armature) return;
|
|
499
|
-
this.#armature = armature;
|
|
500
|
-
this.#bones = armature.bones;
|
|
501
|
-
const rest = new Map(armature.bones.map((bone) => [bone.name, matrixOf(bone.rest ?? bone.matrix)]));
|
|
502
|
-
this.#restRelative = new Map(armature.bones.map((bone) => {
|
|
503
|
-
const own = rest.get(bone.name)!;
|
|
504
|
-
const parent = bone.parent === null ? undefined : rest.get(bone.parent);
|
|
505
|
-
return [bone.name, parent ? parent.clone().invert().multiply(own) : own.clone()];
|
|
506
|
-
}));
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
get armature(): BlenderArmature | null {
|
|
510
|
-
return this.#armature;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/** What this armature does in Blender that the evaluator does not, besides its clips' own
|
|
514
|
-
* (`PoseClip.unsupported`): each named once. */
|
|
515
|
-
unsupported(): string[] {
|
|
516
|
-
const out: string[] = [];
|
|
517
|
-
for (const bone of this.#bones) {
|
|
518
|
-
for (const constraint of bone.constraints ?? []) {
|
|
519
|
-
if (constraint.enabled && constraint.type !== 'DAMPED_TRACK') out.push(`the ${constraintLabel(constraint.type)} constraint "${constraint.name}" on bone "${bone.name}"`);
|
|
520
|
-
}
|
|
521
|
-
if (bone.inherit?.length) out.push(`bone "${bone.name}"'s parenting (${bone.inherit.join(', ')})`);
|
|
522
|
-
}
|
|
523
|
-
for (const bone of this.#armature?.animation?.drivers ?? []) out.push(`the drivers on bone "${bone}"`);
|
|
524
|
-
if (this.#armature?.animation?.tweak) out.push('the NLA in tweak mode (leave it with Tab in the NLA editor)');
|
|
525
|
-
for (const track of this.#armature?.animation?.tracks ?? [])
|
|
526
|
-
for (const strip of track.strips)
|
|
527
|
-
if (!strip.mute && strip.animatedTime) out.push(`the animated strip time of "${strip.name}"`);
|
|
528
|
-
return out;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/** Pose the bones from these layers and constraints; answers each bone's armature-space pose.
|
|
532
|
-
* `write: false` only answers it, leaving the bones as they are (the fidelity check). */
|
|
533
|
-
apply(layers: readonly PoseLayer[], world: ConstraintWorld, write = true): Map<string, THREE.Matrix4> {
|
|
534
|
-
const values = new Map<string, Values>();
|
|
535
|
-
const touched = new Map<string, Record<Channel, number>>();
|
|
536
|
-
for (const bone of this.#bones) {
|
|
537
|
-
const own = bone.channels;
|
|
538
|
-
values.set(bone.name, Object.fromEntries(CHANNELS.map((channel) => [channel, [...(own?.[channel] ?? DEFAULTS[channel])]])) as Values);
|
|
539
|
-
touched.set(bone.name, { location: 0, rotation_quaternion: 0, rotation_euler: 0, rotation_axis_angle: 0, scale: 0 });
|
|
540
|
-
}
|
|
541
|
-
for (const layer of layers) blendLayer(values, touched, layer);
|
|
542
|
-
|
|
543
|
-
this.rig.object.updateWorldMatrix(true, false);
|
|
544
|
-
const armatureWorld = this.rig.object.matrixWorld;
|
|
545
|
-
const toArmature = armatureWorld.clone().invert();
|
|
546
|
-
const poses = new Map<string, THREE.Matrix4>();
|
|
547
|
-
for (const bone of this.#bones) {
|
|
548
|
-
const parent = bone.parent === null ? null : poses.get(bone.parent) ?? null;
|
|
549
|
-
const basis = basisMatrix(bone.channels?.mode ?? 'QUATERNION', values.get(bone.name)!);
|
|
550
|
-
let pose = (parent ? parent.clone() : new THREE.Matrix4()).multiply(this.#restRelative.get(bone.name)!).multiply(basis);
|
|
551
|
-
for (const constraint of bone.constraints ?? []) {
|
|
552
|
-
if (!constraint.enabled || constraint.type !== 'DAMPED_TRACK') continue;
|
|
553
|
-
const change = world.override?.(bone.name, constraint.name);
|
|
554
|
-
const influence = change?.influence ?? constraint.influence;
|
|
555
|
-
if (influence <= 0) continue;
|
|
556
|
-
const target = this.#target(constraint, change, poses, world);
|
|
557
|
-
if (!target) continue;
|
|
558
|
-
const owner = armatureWorld.clone().multiply(pose);
|
|
559
|
-
const solved = dampedTrack(owner, target, constraint.trackAxis ?? 'TRACK_Y');
|
|
560
|
-
pose = toArmature.clone().multiply(blendMatrix(owner, solved, influence));
|
|
561
|
-
}
|
|
562
|
-
poses.set(bone.name, pose);
|
|
563
|
-
}
|
|
564
|
-
if (!write) return poses;
|
|
565
|
-
for (const bone of this.#bones) {
|
|
566
|
-
const own = poses.get(bone.name)!;
|
|
567
|
-
const parent = bone.parent === null ? undefined : poses.get(bone.parent);
|
|
568
|
-
const local = parent ? parent.clone().invert().multiply(own) : own;
|
|
569
|
-
const presented = this.rig.bones.get(bone.name);
|
|
570
|
-
if (presented) local.decompose(presented.position, presented.quaternion, presented.scale);
|
|
571
|
-
}
|
|
572
|
-
this.rig.object.updateMatrixWorld(true);
|
|
573
|
-
return poses;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/** A constraint's target point in world space: an object's origin, or a point along a bone. */
|
|
577
|
-
#target(constraint: NonNullable<BlenderArmatureBone['constraints']>[number], change: ConstraintOverride | undefined,
|
|
578
|
-
poses: ReadonlyMap<string, THREE.Matrix4>, world: ConstraintWorld): THREE.Vector3 | null {
|
|
579
|
-
if (change?.target) {
|
|
580
|
-
change.target.updateWorldMatrix(true, false);
|
|
581
|
-
return new THREE.Vector3().setFromMatrixPosition(change.target.matrixWorld);
|
|
582
|
-
}
|
|
583
|
-
if (change?.target === null || !constraint.target) return null;
|
|
584
|
-
const object = world.object(constraint.target);
|
|
585
|
-
if (!object) return null;
|
|
586
|
-
object.updateWorldMatrix(true, false);
|
|
587
|
-
if (!constraint.subtarget) return new THREE.Vector3().setFromMatrixPosition(object.matrixWorld);
|
|
588
|
-
const own = constraint.target === this.rig.armature;
|
|
589
|
-
const pose = own ? poses.get(constraint.subtarget) : undefined;
|
|
590
|
-
const bone = this.#bones.find((one) => one.name === constraint.subtarget);
|
|
591
|
-
if (own && pose && bone) {
|
|
592
|
-
const along = new THREE.Vector3(0, bone.length * (constraint.headTail ?? 0), 0).applyMatrix4(pose);
|
|
593
|
-
return along.applyMatrix4(this.rig.object.matrixWorld);
|
|
594
|
-
}
|
|
595
|
-
const presented = object.getObjectByName(constraint.subtarget);
|
|
596
|
-
return presented ? new THREE.Vector3().setFromMatrixPosition(presented.matrixWorld) : null;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const matrixOf = (rows: readonly (readonly number[])[]): THREE.Matrix4 =>
|
|
601
|
-
new THREE.Matrix4().set(...(rows.flat() as Parameters<THREE.Matrix4['set']>));
|
|
602
|
-
|
|
603
|
-
const constraintLabel = (type: string): string =>
|
|
604
|
-
type.toLowerCase().split('_').map((word) => (word === 'ik' ? 'IK' : word[0]!.toUpperCase() + word.slice(1))).join(' ');
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* THE FIDELITY CHECK: how far an evaluated pose is from Blender's own at the same frame (the
|
|
608
|
-
* frame's `PoseBone.matrix`). Answers the bone that is furthest off, or null when every bone is
|
|
609
|
-
* within a tenth of a degree and a thousandth of its length.
|
|
610
|
-
*/
|
|
611
|
-
export function poseDivergence(evaluated: ReadonlyMap<string, THREE.Matrix4>, armature: BlenderArmature):
|
|
612
|
-
{ bone: string; degrees: number; offset: number } | null {
|
|
613
|
-
let worst: { bone: string; degrees: number; offset: number; score: number } | null = null;
|
|
614
|
-
const [p0, q0, s0, p1, q1, s1] = [new THREE.Vector3(), new THREE.Quaternion(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Quaternion(), new THREE.Vector3()];
|
|
615
|
-
for (const bone of armature.bones) {
|
|
616
|
-
const ours = evaluated.get(bone.name);
|
|
617
|
-
if (!ours) continue;
|
|
618
|
-
ours.decompose(p0, q0, s0);
|
|
619
|
-
matrixOf(bone.matrix).decompose(p1, q1, s1);
|
|
620
|
-
const degrees = THREE.MathUtils.radToDeg(q0.angleTo(q1));
|
|
621
|
-
// Relative to the bone, but never to less than a tenth of a metre: a fingertip bone a
|
|
622
|
-
// centimetre long is not off by a tenth of a millimetre's float noise.
|
|
623
|
-
const offset = p0.distanceTo(p1) / Math.max(bone.length, 0.1);
|
|
624
|
-
const score = Math.max(degrees / 0.1, offset / 1e-3);
|
|
625
|
-
if (score > 1 && (!worst || score > worst.score)) worst = { bone: bone.name, degrees, offset, score };
|
|
626
|
-
}
|
|
627
|
-
return worst && { bone: worst.bone, degrees: worst.degrees, offset: worst.offset };
|
|
628
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Blender's Properties context remains inspectable without a selection.
|
|
2
2
|
* Reuse the same RNA sections and Properties projection as selected objects. */
|
|
3
|
-
import { registerNullSubjectProvider } from '@volter/sdk/
|
|
3
|
+
import { registerNullSubjectProvider } from '@volter/sdk/inspection/null-subject';
|
|
4
4
|
import { blenderOutlinerState } from './blender-outliner-model';
|
|
5
5
|
import { blenderOutlinerKind } from './blender-outliner-authoring';
|
|
6
6
|
import {
|