@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.
- package/LICENSE +724 -0
- package/README.md +48 -0
- package/browser/blender-emscripten-engine.mts +289 -0
- package/browser/blender-engine.mts +412 -0
- package/browser/blender-wali-engine.mts +362 -0
- package/browser/index.ts +7 -0
- package/browser/protocol.ts +202 -0
- package/browser/rna.ts +697 -0
- package/browser/runtime.ts +511 -0
- package/browser/session-frame.mts +169 -0
- package/browser/session.py +4166 -0
- package/browser/three/agx-base-srgb.lut +0 -0
- package/browser/three/agx-look-medium-high-contrast.lut +0 -0
- package/browser/three/agx-look-punchy.lut +0 -0
- package/browser/three/attach-presenter.ts +140 -0
- package/browser/three/blender-agx.ts +235 -0
- package/browser/three/blender-base64.ts +42 -0
- package/browser/three/blender-corner-normals.ts +432 -0
- package/browser/three/blender-display-lut.ts +145 -0
- package/browser/three/blender-filmic.ts +49 -0
- package/browser/three/blender-frame-columns.ts +100 -0
- package/browser/three/blender-gradient-texture.ts +57 -0
- package/browser/three/blender-runtime-armature.ts +528 -0
- package/browser/three/blender-runtime-frame.ts +39 -0
- package/browser/three/blender-runtime-geometry.ts +342 -0
- package/browser/three/blender-runtime-lighting.ts +829 -0
- package/browser/three/blender-runtime-shadows.ts +107 -0
- package/browser/three/blender-runtime-view.ts +1481 -0
- package/browser/three/blender-runtime-volume.ts +128 -0
- package/browser/three/blender-runtime-weights.ts +306 -0
- package/browser/three/blender-sky.ts +461 -0
- package/browser/three/blender-standard.ts +68 -0
- package/browser/three/blender-triangulate.ts +181 -0
- package/browser/three/filmic-srgb.lut +0 -0
- package/browser/three/presenter.ts +265 -0
- package/browser/three/release.ts +27 -0
- package/browser/three/sky-precompute-worker.ts +45 -0
- package/browser/three/sky-worker.ts +79 -0
- package/browser/three/world-field-sampler.ts +358 -0
- package/browser/three/world-math.ts +59 -0
- package/browser/vgai_three.py +554 -0
- package/browser/worker.ts +648 -0
- package/package.json +48 -0
- package/wasm/BUNDLE.json +65 -0
- package/wasm/DEPENDENCY-LICENSES.txt +4879 -0
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +2 -0
- 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
|
+
}
|