@volter/blender-engine 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +724 -0
  2. package/README.md +48 -0
  3. package/browser/blender-emscripten-engine.mts +289 -0
  4. package/browser/blender-engine.mts +412 -0
  5. package/browser/blender-wali-engine.mts +362 -0
  6. package/browser/index.ts +7 -0
  7. package/browser/protocol.ts +202 -0
  8. package/browser/rna.ts +697 -0
  9. package/browser/runtime.ts +511 -0
  10. package/browser/session-frame.mts +169 -0
  11. package/browser/session.py +4166 -0
  12. package/browser/three/agx-base-srgb.lut +0 -0
  13. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  14. package/browser/three/agx-look-punchy.lut +0 -0
  15. package/browser/three/attach-presenter.ts +140 -0
  16. package/browser/three/blender-agx.ts +235 -0
  17. package/browser/three/blender-base64.ts +42 -0
  18. package/browser/three/blender-corner-normals.ts +432 -0
  19. package/browser/three/blender-display-lut.ts +145 -0
  20. package/browser/three/blender-filmic.ts +49 -0
  21. package/browser/three/blender-frame-columns.ts +100 -0
  22. package/browser/three/blender-gradient-texture.ts +57 -0
  23. package/browser/three/blender-runtime-armature.ts +528 -0
  24. package/browser/three/blender-runtime-frame.ts +39 -0
  25. package/browser/three/blender-runtime-geometry.ts +342 -0
  26. package/browser/three/blender-runtime-lighting.ts +829 -0
  27. package/browser/three/blender-runtime-shadows.ts +107 -0
  28. package/browser/three/blender-runtime-view.ts +1481 -0
  29. package/browser/three/blender-runtime-volume.ts +128 -0
  30. package/browser/three/blender-runtime-weights.ts +306 -0
  31. package/browser/three/blender-sky.ts +461 -0
  32. package/browser/three/blender-standard.ts +68 -0
  33. package/browser/three/blender-triangulate.ts +181 -0
  34. package/browser/three/filmic-srgb.lut +0 -0
  35. package/browser/three/presenter.ts +265 -0
  36. package/browser/three/release.ts +27 -0
  37. package/browser/three/sky-precompute-worker.ts +45 -0
  38. package/browser/three/sky-worker.ts +79 -0
  39. package/browser/three/world-field-sampler.ts +358 -0
  40. package/browser/three/world-math.ts +59 -0
  41. package/browser/vgai_three.py +554 -0
  42. package/browser/worker.ts +648 -0
  43. package/package.json +48 -0
  44. package/wasm/BUNDLE.json +65 -0
  45. package/wasm/DEPENDENCY-LICENSES.txt +4879 -0
  46. package/wasm/blender_browser.data.br +0 -0
  47. package/wasm/blender_browser.js +2 -0
  48. package/wasm/blender_browser.wasm.br +0 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * THE RUNTIME FRAME'S COLUMN CONTRACT — the typed arrays a `runtime_present`
3
+ * ships and `drawArraysFromColumns` draws from.
4
+ *
5
+ * WHERE IT CAME FROM, and why it lives here now. The columns are Blender's
6
+ * own export arena (`@volter/blender-engine`'s `browser/session-frame.mts`, `columnsToTypedArrays`):
7
+ * `{offset, length, dtype, count, stride}` descriptors into one transferable
8
+ * buffer, decoded on this side into the arrays below. The presenter was typed
9
+ * against the retired mesh kit's `mesh-store.ts` because that store happened to
10
+ * hold the same layout — Blender's — and the two were in one package. They
11
+ * are not the same contract: the kit's store is MODELING STATE it mutates
12
+ * (selection, hide flags, creases, an allocator per attribute type), while
13
+ * this is the READ-ONLY shape of one presented frame. This file is the second
14
+ * half only, so `@volter/editor-blender` names no modeling package (WORK.md §Blender in
15
+ * the tab is Blender, "The mesh kit retires", M1).
16
+ *
17
+ * Layout (Blender's own): positions `co` (3 per vertex); faces as CSR —
18
+ * `faceStart` (nf+1) into `corner` (one vertex index per loop), a loop's edge
19
+ * in `cornerEdge`; every mesh edge as a pair in `edge` (2 per edge) in stored
20
+ * order with a per-edge `sharp` flag; per-face `material` and `smooth`; every
21
+ * generic attribute as one typed column per layer, in `mesh.attributes` order,
22
+ * with the active and render UV names.
23
+ */
24
+
25
+ export type Domain = 'POINT' | 'EDGE' | 'FACE' | 'CORNER';
26
+
27
+ export type AttributeType =
28
+ | 'FLOAT'
29
+ | 'INT'
30
+ | 'INT8'
31
+ | 'BOOLEAN'
32
+ | 'FLOAT2'
33
+ | 'FLOAT_VECTOR'
34
+ | 'FLOAT_COLOR'
35
+ | 'BYTE_COLOR'
36
+ | 'INT16_2D'
37
+ | 'INT32_2D'
38
+ | 'QUATERNION'
39
+ | 'FLOAT4X4'
40
+ | 'FLOAT4'
41
+ | 'STRING';
42
+
43
+ export type AttributeData =
44
+ | Float32Array
45
+ | Int32Array
46
+ | Int8Array
47
+ | Int16Array
48
+ | Uint8Array
49
+ | string[];
50
+
51
+ /** One generic attribute layer: `size` components per element, stored flat. */
52
+ export interface AttributeColumn {
53
+ name: string;
54
+ type: AttributeType;
55
+ domain: Domain;
56
+ data: AttributeData;
57
+ }
58
+
59
+ /** Components per element. The presenter reads a column, never allocates one,
60
+ * so this table carries the stride and nothing else. */
61
+ export const ATTRIBUTE_LAYOUT: Record<AttributeType, { size: number }> = {
62
+ FLOAT: { size: 1 },
63
+ INT: { size: 1 },
64
+ INT8: { size: 1 },
65
+ BOOLEAN: { size: 1 },
66
+ FLOAT2: { size: 2 },
67
+ FLOAT_VECTOR: { size: 3 },
68
+ FLOAT_COLOR: { size: 4 },
69
+ BYTE_COLOR: { size: 4 },
70
+ INT16_2D: { size: 2 },
71
+ INT32_2D: { size: 2 },
72
+ QUATERNION: { size: 4 },
73
+ FLOAT4X4: { size: 16 },
74
+ FLOAT4: { size: 4 },
75
+ STRING: { size: 1 },
76
+ };
77
+
78
+ /** Exactly what the draw reads off a presented mesh. */
79
+ export interface MeshColumns {
80
+ /** 3 * nv */
81
+ co: Float64Array;
82
+ /** nf + 1 */
83
+ faceStart: Uint32Array;
84
+ /** faceStart[nf] entries: vertex index per loop */
85
+ corner: Uint32Array;
86
+ /** same length: edge index per loop (-1 unknown) */
87
+ cornerEdge: Int32Array;
88
+ /** 2 * ne */
89
+ edge: Uint32Array;
90
+ /** ne */
91
+ edgeSharp: Uint8Array;
92
+ /** nf */
93
+ material: Uint32Array;
94
+ /** nf */
95
+ smooth: Uint8Array;
96
+ /** Generic attribute layers in `mesh.attributes` order. */
97
+ attributes: AttributeColumn[];
98
+ activeUv: string | null;
99
+ renderUv: string | null;
100
+ }
@@ -0,0 +1,57 @@
1
+ /** Blender's Gradient **Type**. */
2
+ export type GradientType =
3
+ | 'LINEAR'
4
+ | 'QUADRATIC'
5
+ | 'EASING'
6
+ | 'DIAGONAL'
7
+ | 'SPHERICAL'
8
+ | 'QUADRATIC_SPHERE'
9
+ | 'RADIAL';
10
+
11
+ /** Options for `gradientTexture` (Blender: the Gradient Texture node). */
12
+ export interface GradientTextureOptions {
13
+ /** Blender's **Type**. Default `'LINEAR'` (Blender's own). */
14
+ readonly type?: GradientType;
15
+ }
16
+
17
+ /**
18
+ * A RAMP IN SPACE (Blender: the **Gradient Texture** node): the point's own
19
+ * coordinates read as a 0–1 fac. Height tinting, a spherical falloff, a
20
+ * radial sweep — the cheapest way to say "this end is different".
21
+ *
22
+ * NO SCALE OR CENTRE, exactly like Blender's node, which takes a Vector and
23
+ * nothing else: position, rotate and scale the POINT before calling (that
24
+ * is what Blender's Mapping node does).
25
+ *
26
+ * This one IS numerically Blender's — the seven ramps are closed-form and
27
+ * there is no noise substrate to differ over.
28
+ */
29
+ export function gradientTexture(
30
+ x: number,
31
+ y: number,
32
+ z: number,
33
+ options: GradientTextureOptions = {},
34
+ ): number {
35
+ switch (options.type ?? 'LINEAR') {
36
+ case 'QUADRATIC': {
37
+ const r = Math.max(x, 0);
38
+ return r * r;
39
+ }
40
+ case 'EASING': {
41
+ const r = Math.min(1, Math.max(0, x));
42
+ return r * r * (3 - 2 * r);
43
+ }
44
+ case 'DIAGONAL':
45
+ return (x + y) / 2;
46
+ case 'SPHERICAL':
47
+ return Math.max(0.999999 - Math.hypot(x, y, z), 0);
48
+ case 'QUADRATIC_SPHERE': {
49
+ const r = Math.max(0.999999 - Math.hypot(x, y, z), 0);
50
+ return r * r;
51
+ }
52
+ case 'RADIAL':
53
+ return Math.atan2(y, x) / (2 * Math.PI) + 0.5;
54
+ default:
55
+ return x;
56
+ }
57
+ }
@@ -0,0 +1,528 @@
1
+ /**
2
+ * THE ARMATURE OVERLAY — Blender's bones, drawn in three.js over the engine's
3
+ * own data (ARCHITECTURE-CORE §Blender north star, "Inspection parity, not
4
+ * editing parity" and "The reference is Blender's SOURCE as well as its
5
+ * frames"; WORK.md §Blender in the tab is Blender, "Inspection parity", I4).
6
+ *
7
+ * Blender's overlay ENGINE is never run, ported or recorded: what crosses is
8
+ * the data its draw functions read — per bone a pose matrix, a length, a
9
+ * parent, its hide/select/active flags — and this file is our own drawing of
10
+ * the same shapes with Blender's own vertex tables and Blender's own theme
11
+ * colours. Every metric below cites the file and constant it came from, at the
12
+ * engine's pin (Blender 5.2.0, `fbe6228777e7`).
13
+ *
14
+ * WHAT IS DRAWN, and what is not. `display_type` OCTAHEDRAL and STICK are
15
+ * drawn (`bone_draw_octa` / `bone_draw_line`, `overlay_armature.cc:1415-1500`).
16
+ * BBONE, ENVELOPE and WIRE fall back to the octahedron and say so through a
17
+ * frame warning — Blender's B-Bone needs the per-segment matrices
18
+ * (`draw_bone_update_disp_matrix_bbone`, `:1186`) and its envelope needs the
19
+ * head/tail radii, neither of which the door carries yet. A standing named
20
+ * warning, never a silent degrade.
21
+ *
22
+ * EDIT MODE IS NOT DRAWN EITHER, for a reason worth stating: in edit mode
23
+ * Blender draws the EDIT bones (`ED_armature_ebone_to_mat4`), which are a
24
+ * different set of matrices from `pose.bones`, and drawing the pose there
25
+ * would be a confident picture of the wrong thing. The door reports the mode;
26
+ * an armature in EDIT mode draws its pose with the object-mode colours and the
27
+ * frame carries the warning.
28
+ */
29
+ import * as THREE from 'three';
30
+ import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
31
+ import { LineSegments2 } from 'three/addons/lines/LineSegments2.js';
32
+ import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
33
+ import { z } from 'zod';
34
+
35
+ const scalar = z.number().finite();
36
+
37
+ /** ONE BONE, as `session.py`'s `_armature_bones` answers it. */
38
+ export const boneSchema = z
39
+ .object({
40
+ name: z.string(),
41
+ parent: z.string().nullable(),
42
+ connected: z.boolean(),
43
+ hide: z.boolean(),
44
+ length: scalar,
45
+ /** `PoseBone.matrix` — `pchan->pose_mat`, in the armature OBJECT's space,
46
+ * row-major as four rows. `draw_bone_update_disp_matrix_default`
47
+ * (`overlay_armature.cc:990-1020`) is this matrix rescaled by `length`. */
48
+ matrix: z.array(z.tuple([scalar, scalar, scalar, scalar])).length(4),
49
+ select: z.boolean(),
50
+ active: z.boolean(),
51
+ lockedWeight: z.boolean(),
52
+ })
53
+ .strict();
54
+
55
+ export const armatureSchema = z
56
+ .object({
57
+ object: z.string(),
58
+ /** `bArmature.drawtype`, `rna_armature.cc:2146-2168`. */
59
+ displayType: z.enum(['OCTAHEDRAL', 'STICK', 'BBONE', 'ENVELOPE', 'WIRE']),
60
+ /** `Object.dtx & OB_DRAW_IN_FRONT` (`rna_object.cc:3646-3648`). */
61
+ showInFront: z.boolean(),
62
+ mode: z.string(),
63
+ bones: z.array(boneSchema),
64
+ })
65
+ .strict();
66
+
67
+ export type BlenderArmature = z.infer<typeof armatureSchema>;
68
+ type Bone = z.infer<typeof boneSchema>;
69
+
70
+ /**
71
+ * BLENDER'S OWN THEME BYTES, from `release/datafiles/userdef/
72
+ * userdef_default_theme.c`'s `.space_view3d` block. They are written here as
73
+ * the sRGB hex the theme table holds, because that is the pixel Blender's
74
+ * overlay pass puts on screen: `ui::theme::get_color_4fv` divides the stored
75
+ * bytes by 255 with no transfer function and the overlay framebuffer is
76
+ * display-referred, so the on-screen value IS the byte.
77
+ */
78
+ export const BONE_THEME = {
79
+ /** `.bone_solid`, `:404`. The octahedron's fill. */
80
+ solid: 0xb2b2b2,
81
+ /** `.bone_pose`, `:405` — a SELECTED pose bone's wire. */
82
+ pose: 0x50c8ff,
83
+ /** `.bone_pose_active`, `:406` — active AND selected. */
84
+ poseActive: 0x8cffff,
85
+ /**
86
+ * `bone_pose_active_unsel` — active, not selected. NOT a theme key: it is
87
+ * `get_color_blend_shade_4fv(TH_WIRE, TH_BONE_POSE, 0.15, 0)`
88
+ * (`overlay_instance.cc:324-325`), i.e. 15% of `.bone_pose` over `.wire`
89
+ * (#000000), which is 0x50·0.15=0x0c, 0xc8·0.15=0x1e, 0xff·0.15=0x26.
90
+ */
91
+ poseActiveUnsel: 0x0c1e26,
92
+ /** `.wire`, `:377` — an UNSELECTED pose bone's wire. */
93
+ wire: 0x000000,
94
+ /** `.vertex`, `:386` — `get_bone_wire_color`'s `ARM_DRAW_MODE_OBJECT` branch
95
+ * (`overlay_armature.cc:933`) takes `theme.colors.vert` for every bone of an
96
+ * armature that is not the one being posed. */
97
+ vertex: 0x000000,
98
+ /** `.bone_locked_weight`, `:407`. The alpha is the BLEND FACTOR
99
+ * (`bone_locked_color_shade`, `:850-856`: `interp_v3_v3v3(color, color,
100
+ * locked, locked[3])`), not a draw alpha — 0x80/255. */
101
+ lockedWeight: 0xff0000,
102
+ lockedWeightFactor: 0x80 / 255,
103
+ } as const;
104
+
105
+ /**
106
+ * `bone_hint_color_shade` (`overlay_armature.cc:2143-2150` in this pin's
107
+ * numbering, the function right under `get_bone_wire_color`): the shape's
108
+ * shaded side is the colour SQUARED and scaled by 0.1 — "increase contrast",
109
+ * then "decrease value to add more shading to the shape".
110
+ */
111
+ function hintColor(color: THREE.Color): THREE.Color {
112
+ return new THREE.Color(color.r * color.r * 0.1, color.g * color.g * 0.1, color.b * color.b * 0.1);
113
+ }
114
+
115
+ /**
116
+ * BLENDER'S OCTAHEDRON, vertex for vertex (`overlay_shape.cc:96-131`).
117
+ *
118
+ * Bone space: the bone runs from the origin along +Y and is one unit long, so
119
+ * the waist ring sits at y = 0.1 with a 0.1 half-width in x and z. The display
120
+ * matrix scales the whole thing by the bone's length, which is why these are
121
+ * the numbers and not a proportion of anything.
122
+ */
123
+ const OCTAHEDRAL_VERTS: readonly (readonly [number, number, number])[] = [
124
+ [0, 0, 0],
125
+ [0.1, 0.1, 0.1],
126
+ [0.1, 0.1, -0.1],
127
+ [-0.1, 0.1, -0.1],
128
+ [-0.1, 0.1, 0.1],
129
+ [0, 1, 0],
130
+ ];
131
+
132
+ /** `bone_octahedral_solid_tris`, `overlay_shape.cc:120-131`. */
133
+ const OCTAHEDRAL_TRIS: readonly (readonly [number, number, number])[] = [
134
+ [2, 1, 0],
135
+ [3, 2, 0],
136
+ [4, 3, 0],
137
+ [1, 4, 0],
138
+ [5, 1, 2],
139
+ [5, 2, 3],
140
+ [5, 3, 4],
141
+ [5, 4, 1],
142
+ ];
143
+
144
+ /** `bone_octahedral_solid_normals`, `overlay_shape.cc:159-169`, one per tri. */
145
+ const SQRT1_2 = Math.SQRT1_2;
146
+ const OCTAHEDRAL_NORMALS: readonly (readonly [number, number, number])[] = [
147
+ [SQRT1_2, -SQRT1_2, 0],
148
+ [0, -SQRT1_2, -SQRT1_2],
149
+ [-SQRT1_2, -SQRT1_2, 0],
150
+ [0, -SQRT1_2, SQRT1_2],
151
+ [0.99388373, 0.11043154, 0],
152
+ [0, 0.11043154, -0.99388373],
153
+ [-0.99388373, 0.11043154, 0],
154
+ [0, 0.11043154, 0.99388373],
155
+ ];
156
+
157
+ /** `bone_octahedral_wire_lines`, `overlay_shape.cc:105-118` — the shape's own
158
+ * twelve edges. Blender's default pass draws only the SILHOUETTE subset of
159
+ * them, recomputed per view in a geometry shader
160
+ * (`overlay_armature_shape_outline_vert.glsl`); the whole edge list is what
161
+ * its own table holds and what it draws in wire/X-ray, and it is what this
162
+ * presenter draws, stated rather than approximated silently. */
163
+ const OCTAHEDRAL_LINES: readonly (readonly [number, number])[] = [
164
+ [0, 1],
165
+ [1, 5],
166
+ [5, 3],
167
+ [3, 0],
168
+ [0, 4],
169
+ [4, 5],
170
+ [5, 2],
171
+ [2, 0],
172
+ [1, 2],
173
+ [2, 3],
174
+ [3, 4],
175
+ [4, 1],
176
+ ];
177
+
178
+ /** `#define rad 0.05f` — `overlay_armature_sphere_solid_vert.glsl:15`, in the
179
+ * bone's own display space, so the drawn radius is 0.05 × the bone's length. */
180
+ const BONE_POINT_RADIUS = 0.05;
181
+
182
+ /** `get_bone_wire_thickness` (`overlay_armature.cc:884-894`): 2 for a selected
183
+ * or active bone, 1 otherwise. Blender carries it in the wire colour's alpha
184
+ * channel; here it is the line width in CSS pixels, which is what it means. */
185
+ function wireThickness(bone: Bone): number {
186
+ return bone.select || bone.active ? 2 : 1;
187
+ }
188
+
189
+ /** `stick_size = theme.sizes.pixel * 5.0f`
190
+ * (`overlay_armature_stick_vert.glsl:71`) over a strip whose half-width is 1
191
+ * (`overlay_shape.cc:368-375`): a 10 CSS px bar at UI scale 1. */
192
+ const STICK_WIDTH = 10;
193
+
194
+ /**
195
+ * The CORE of that bar, in the bone's own colour.
196
+ *
197
+ * Blender's stick fragment is a gradient, not two bands:
198
+ * `fac = smoothstep(1.0, 0.2, color_fac)` then `mix(inner, wire, fac)`
199
+ * (`overlay_armature_stick_frag.glsl:13-14`), where `color_fac` runs 1 at the
200
+ * centre line to 0 at either edge. The blend passes 50% at `color_fac` 0.6,
201
+ * i.e. 0.4 of the half-width — so the band that is more bone than wire is
202
+ * 0.4 × 10 = 4 px. `LineMaterial` draws a flat width, so the bar is two
203
+ * lines, 10 px of wire colour with a 4 px core, and that 4 is this
204
+ * measurement rather than a choice.
205
+ */
206
+ const STICK_CORE_WIDTH = 4;
207
+
208
+ /**
209
+ * THE SOLID BONE'S SHADING, ported formula for formula from
210
+ * `overlay_armature_shape_solid_vert.glsl:28-36`. A three.js material because
211
+ * the value is VIEW-dependent (the normal is taken to view space), so it
212
+ * cannot be baked into a vertex colour on the way in.
213
+ *
214
+ * `light` is deliberately un-normalised there and is left so here; `s` is the
215
+ * shader's own 0.2 smooth-lighting floor; the mix is by `fac * fac`.
216
+ */
217
+ const BONE_SOLID_VERTEX = /* glsl */ `
218
+ attribute vec3 aSolid;
219
+ attribute vec3 aHint;
220
+ varying vec3 vColor;
221
+ void main() {
222
+ vec3 n = normalize(normalMatrix * normal);
223
+ float d = dot(n, vec3(0.1, 0.1, 0.8));
224
+ float fac = clamp(d * 0.8 + 0.2, 0.0, 1.0);
225
+ vColor = mix(aHint, aSolid, fac * fac);
226
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
227
+ }
228
+ `;
229
+ /**
230
+ * THE OUTPUT TRANSFORM HAS TO BE ASKED FOR, and forgetting it is what the walk
231
+ * caught: a `ShaderMaterial` writes `gl_FragColor` RAW. three's own materials
232
+ * end with `#include <colorspace_fragment>`, which is what converts the linear
233
+ * working value to the renderer's `outputColorSpace`; without it the theme's
234
+ * `bone_solid` (#b2b2b2, linear 0.44) reached the framebuffer as 0.44 and the
235
+ * bones photographed at byte 112 against Blender's 178 — visibly dark grey
236
+ * where Blender's are light. Tone mapping is deliberately NOT included: an
237
+ * overlay is drawn after the view transform in Blender and is not a
238
+ * scene-referred value.
239
+ *
240
+ * ONLY THE `_fragment` HALF: three PREPENDS `colorspace_pars_fragment` to every
241
+ * `ShaderMaterial` already, so including it here declared `LinearTransferOETF`,
242
+ * `sRGBTransferEOTF` and `sRGBTransferOETF` twice and the whole program refused
243
+ * to compile ("function already has a body", measured live in `vgai console`).
244
+ */
245
+ const BONE_SOLID_FRAGMENT = /* glsl */ `
246
+ varying vec3 vColor;
247
+ void main() {
248
+ gl_FragColor = vec4(vColor, 1.0);
249
+ #include <colorspace_fragment>
250
+ }
251
+ `;
252
+
253
+ function color(hex: number): THREE.Color {
254
+ // The theme byte IS the screen pixel (see BONE_THEME), so it is read as
255
+ // sRGB and three's colour management produces that pixel back.
256
+ return new THREE.Color().setHex(hex, THREE.SRGBColorSpace);
257
+ }
258
+
259
+ /**
260
+ * `get_bone_wire_color` (`overlay_armature.cc:906-940`) with no custom bone
261
+ * colour set (`ARM_COL_CUSTOM` clear, which is every armature this document
262
+ * has shown), plus `bone_locked_color_shade` for the weight-paint lock.
263
+ */
264
+ function boneWireColor(armature: BlenderArmature, bone: Bone): THREE.Color {
265
+ const pose = armature.mode === 'POSE';
266
+ let value: THREE.Color;
267
+ if (!pose) {
268
+ // ARM_DRAW_MODE_OBJECT: `copy_v3_v3(disp_color, theme.colors.vert)`.
269
+ value = color(BONE_THEME.vertex);
270
+ } else if (bone.active && bone.select) value = color(BONE_THEME.poseActive);
271
+ else if (bone.active) value = color(BONE_THEME.poseActiveUnsel);
272
+ else if (bone.select) value = color(BONE_THEME.pose);
273
+ else value = color(BONE_THEME.wire);
274
+ if (pose && bone.lockedWeight)
275
+ value.lerp(color(BONE_THEME.lockedWeight), BONE_THEME.lockedWeightFactor);
276
+ return value;
277
+ }
278
+
279
+ /** `get_bone_solid_color` (`:860-875`): `theme.colors.bone_solid`, shaded
280
+ * toward the locked colour in pose mode when the bone's group is locked. */
281
+ function boneSolidColor(armature: BlenderArmature, bone: Bone): THREE.Color {
282
+ const value = color(BONE_THEME.solid);
283
+ if (armature.mode === 'POSE' && bone.lockedWeight)
284
+ value.lerp(color(BONE_THEME.lockedWeight), BONE_THEME.lockedWeightFactor);
285
+ return value;
286
+ }
287
+
288
+ const tempMatrix = new THREE.Matrix4();
289
+ const tempVector = new THREE.Vector3();
290
+ const tempNormal = new THREE.Matrix3();
291
+
292
+ /** The bone's DISPLAY matrix in the document's own space: the armature
293
+ * object's world matrix times the pose matrix, then the uniform rescale by
294
+ * the bone's length that `draw_bone_update_disp_matrix_default` applies. */
295
+ function displayMatrix(objectMatrix: THREE.Matrix4, bone: Bone): THREE.Matrix4 {
296
+ const pose = new THREE.Matrix4().set(...(bone.matrix.flat() as Parameters<THREE.Matrix4['set']>));
297
+ return pose.premultiply(objectMatrix).scale(new THREE.Vector3().setScalar(bone.length));
298
+ }
299
+
300
+ /**
301
+ * THE OVERLAY, rebuilt whenever the frame's armatures change.
302
+ *
303
+ * It is rebuilt rather than reconciled because a pose moves every bone at once
304
+ * — there is no "some bones changed" case — and the whole drawing of a
305
+ * 200-bone rig is a few thousand vertices. Resources are owned here and freed
306
+ * on the next build, which is the same contract `BlenderRuntimeView` holds for
307
+ * its geometries.
308
+ */
309
+ export class ArmatureOverlay {
310
+ readonly group = new THREE.Group();
311
+ private readonly disposables: { dispose(): void }[] = [];
312
+ private signature = '';
313
+
314
+ constructor() {
315
+ this.group.name = 'BlenderArmatureOverlay';
316
+ }
317
+
318
+ /**
319
+ * Draw these armatures. `objectMatrix` answers the armature OBJECT's world
320
+ * matrix in BLENDER's frame — the frame's own `objects[].matrix`, never the
321
+ * presented three object's, because the presenter reparents objects and
322
+ * premultiplies by the parent's inverse while a bone's pose matrix is in the
323
+ * armature's own space.
324
+ *
325
+ * Returns the warnings this drawing could not honour, by name.
326
+ */
327
+ apply(
328
+ armatures: Record<string, BlenderArmature>,
329
+ objectMatrix: (name: string) => THREE.Matrix4 | null,
330
+ ): readonly string[] {
331
+ const warnings: string[] = [];
332
+ const signature = JSON.stringify(armatures);
333
+ if (signature === this.signature) return warnings;
334
+ this.signature = signature;
335
+ this.clear();
336
+ for (const armature of Object.values(armatures)) {
337
+ const matrix = objectMatrix(armature.object);
338
+ if (matrix === null) continue;
339
+ if (armature.displayType !== 'OCTAHEDRAL' && armature.displayType !== 'STICK')
340
+ warnings.push(
341
+ `armature ${armature.object}: Blender's ${armature.displayType} bone display needs data ` +
342
+ 'this door does not carry (B-Bone segment matrices, envelope radii), so its bones are ' +
343
+ 'drawn octahedral',
344
+ );
345
+ if (armature.mode === 'EDIT')
346
+ warnings.push(
347
+ `armature ${armature.object}: in Edit Mode Blender draws the EDIT bones, and this ` +
348
+ 'overlay draws the pose — the shapes are the rest pose, not the edited one',
349
+ );
350
+ const bones = armature.bones.filter((bone) => !bone.hide);
351
+ if (bones.length === 0) continue;
352
+ const node =
353
+ armature.displayType === 'STICK'
354
+ ? this.buildStick(armature, bones, matrix)
355
+ : this.buildOctahedral(armature, bones, matrix);
356
+ // THE IN-FRONT LAYER: Blender puts an armature whose `show_in_front` is
357
+ // set into the overlay pass whose depth buffer is cleared first
358
+ // (`Instance::object_is_in_front`, `overlay_instance.cc:1110-1115`), so
359
+ // it draws over everything. Here that is no depth test and a render
360
+ // order past the model's.
361
+ if (armature.showInFront)
362
+ node.traverse((child) => {
363
+ const material = (child as THREE.Mesh).material as THREE.Material | undefined;
364
+ if (material && 'depthTest' in material) material.depthTest = false;
365
+ child.renderOrder = 10;
366
+ });
367
+ this.group.add(node);
368
+ }
369
+ return warnings;
370
+ }
371
+
372
+ private buildOctahedral(
373
+ armature: BlenderArmature,
374
+ bones: readonly Bone[],
375
+ objectMatrix: THREE.Matrix4,
376
+ ): THREE.Object3D {
377
+ const node = new THREE.Group();
378
+ node.name = `${armature.object}:bones`;
379
+ const positions: number[] = [];
380
+ const normals: number[] = [];
381
+ const solids: number[] = [];
382
+ const hints: number[] = [];
383
+ const lineStarts: number[] = [];
384
+ const lineColors: number[] = [];
385
+ // Blender draws every bone's wire at its own thickness; `LineMaterial` is
386
+ // one width per material, so the bones are gathered into the two widths
387
+ // `get_bone_wire_thickness` can answer.
388
+ const thickLines: number[] = [];
389
+ const thickColors: number[] = [];
390
+ const points: THREE.Matrix4[] = [];
391
+ const pointColors: THREE.Color[] = [];
392
+ for (const bone of bones) {
393
+ const matrix = displayMatrix(objectMatrix, bone);
394
+ const solid = boneSolidColor(armature, bone);
395
+ const hint = hintColor(solid);
396
+ const wire = boneWireColor(armature, bone);
397
+ tempNormal.setFromMatrix4(matrix).invert().transpose();
398
+ for (let tri = 0; tri < OCTAHEDRAL_TRIS.length; tri++) {
399
+ const normal = tempVector
400
+ .set(...(OCTAHEDRAL_NORMALS[tri] as [number, number, number]))
401
+ .applyMatrix3(tempNormal)
402
+ .normalize()
403
+ .clone();
404
+ for (const index of OCTAHEDRAL_TRIS[tri]!) {
405
+ const vertex = new THREE.Vector3(
406
+ ...(OCTAHEDRAL_VERTS[index] as [number, number, number]),
407
+ ).applyMatrix4(matrix);
408
+ positions.push(vertex.x, vertex.y, vertex.z);
409
+ normals.push(normal.x, normal.y, normal.z);
410
+ solids.push(solid.r, solid.g, solid.b);
411
+ hints.push(hint.r, hint.g, hint.b);
412
+ }
413
+ }
414
+ const into = wireThickness(bone) === 2 ? thickLines : lineStarts;
415
+ const intoColors = wireThickness(bone) === 2 ? thickColors : lineColors;
416
+ for (const [a, b] of OCTAHEDRAL_LINES) {
417
+ const from = new THREE.Vector3(
418
+ ...(OCTAHEDRAL_VERTS[a] as [number, number, number]),
419
+ ).applyMatrix4(matrix);
420
+ const to = new THREE.Vector3(
421
+ ...(OCTAHEDRAL_VERTS[b] as [number, number, number]),
422
+ ).applyMatrix4(matrix);
423
+ into.push(from.x, from.y, from.z, to.x, to.y, to.z);
424
+ intoColors.push(wire.r, wire.g, wire.b, wire.r, wire.g, wire.b);
425
+ }
426
+ // `draw_points` (`overlay_armature.cc:1337-1373`): the ROOT sphere only
427
+ // for a bone that is not connected to its parent, the TIP sphere always.
428
+ if (!(bone.parent !== null && bone.connected)) {
429
+ points.push(matrix.clone());
430
+ pointColors.push(solid);
431
+ }
432
+ points.push(matrix.clone().multiply(tempMatrix.makeTranslation(0, 1, 0)));
433
+ pointColors.push(solid);
434
+ }
435
+ node.add(this.solidMesh(positions, normals, solids, hints));
436
+ if (lineStarts.length) node.add(this.lines(lineStarts, lineColors, 1));
437
+ if (thickLines.length) node.add(this.lines(thickLines, thickColors, 2));
438
+ for (let i = 0; i < points.length; i++) node.add(this.point(points[i]!, pointColors[i]!));
439
+ return node;
440
+ }
441
+
442
+ private buildStick(
443
+ armature: BlenderArmature,
444
+ bones: readonly Bone[],
445
+ objectMatrix: THREE.Matrix4,
446
+ ): THREE.Object3D {
447
+ const node = new THREE.Group();
448
+ node.name = `${armature.object}:bones`;
449
+ const bar: number[] = [];
450
+ const barColors: number[] = [];
451
+ const core: number[] = [];
452
+ const coreColors: number[] = [];
453
+ for (const bone of bones) {
454
+ const matrix = displayMatrix(objectMatrix, bone);
455
+ // `drw_shgroup_bone_stick` (`overlay_armature.cc:243-263`): head is the
456
+ // display matrix's location, tail is head + its Y axis — which, after
457
+ // the length rescale, is exactly the bone's tail.
458
+ const head = new THREE.Vector3().setFromMatrixPosition(matrix);
459
+ const tail = new THREE.Vector3(0, 1, 0).applyMatrix4(matrix);
460
+ const wire = boneWireColor(armature, bone);
461
+ const solid = boneSolidColor(armature, bone);
462
+ bar.push(head.x, head.y, head.z, tail.x, tail.y, tail.z);
463
+ barColors.push(wire.r, wire.g, wire.b, wire.r, wire.g, wire.b);
464
+ core.push(head.x, head.y, head.z, tail.x, tail.y, tail.z);
465
+ coreColors.push(solid.r, solid.g, solid.b, solid.r, solid.g, solid.b);
466
+ }
467
+ node.add(this.lines(bar, barColors, STICK_WIDTH));
468
+ node.add(this.lines(core, coreColors, STICK_CORE_WIDTH));
469
+ return node;
470
+ }
471
+
472
+ private solidMesh(
473
+ positions: number[],
474
+ normals: number[],
475
+ solids: number[],
476
+ hints: number[],
477
+ ): THREE.Mesh {
478
+ const geometry = new THREE.BufferGeometry();
479
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
480
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
481
+ geometry.setAttribute('aSolid', new THREE.Float32BufferAttribute(solids, 3));
482
+ geometry.setAttribute('aHint', new THREE.Float32BufferAttribute(hints, 3));
483
+ const material = new THREE.ShaderMaterial({
484
+ vertexShader: BONE_SOLID_VERTEX,
485
+ fragmentShader: BONE_SOLID_FRAGMENT,
486
+ // `overlay_armature_shape_solid_frag.glsl` discards the back face by
487
+ // hand; three's own culling is the same answer for a non-inverted
488
+ // matrix, which every armature here has.
489
+ side: THREE.FrontSide,
490
+ });
491
+ this.disposables.push(geometry, material);
492
+ const mesh = new THREE.Mesh(geometry, material);
493
+ mesh.frustumCulled = false;
494
+ return mesh;
495
+ }
496
+
497
+ private lines(positions: number[], colors: number[], width: number): THREE.Object3D {
498
+ const geometry = new LineSegmentsGeometry();
499
+ geometry.setPositions(positions);
500
+ geometry.setColors(colors);
501
+ const material = new LineMaterial({ linewidth: width, vertexColors: true });
502
+ this.disposables.push(geometry, material);
503
+ const lines = new LineSegments2(geometry, material);
504
+ lines.frustumCulled = false;
505
+ return lines;
506
+ }
507
+
508
+ private point(matrix: THREE.Matrix4, fill: THREE.Color): THREE.Mesh {
509
+ const geometry = new THREE.SphereGeometry(BONE_POINT_RADIUS, 12, 8);
510
+ const material = new THREE.MeshBasicMaterial({ color: fill });
511
+ this.disposables.push(geometry, material);
512
+ const mesh = new THREE.Mesh(geometry, material);
513
+ mesh.applyMatrix4(matrix);
514
+ mesh.frustumCulled = false;
515
+ return mesh;
516
+ }
517
+
518
+ private clear(): void {
519
+ for (const child of [...this.group.children]) this.group.remove(child);
520
+ for (const value of this.disposables) value.dispose();
521
+ this.disposables.length = 0;
522
+ }
523
+
524
+ dispose(): void {
525
+ this.clear();
526
+ this.signature = '';
527
+ }
528
+ }