@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,128 @@
1
+ /** Draw the Python-owned density grid directly as a Three.js volume. */
2
+ import * as THREE from 'three';
3
+ import { z } from 'zod';
4
+
5
+ const scalar = z.number().finite();
6
+ const vector = z.tuple([scalar, scalar, scalar]);
7
+ export const volumeSchema = z
8
+ .object({
9
+ min: z.tuple([z.number().int(), z.number().int(), z.number().int()]),
10
+ max: z.tuple([z.number().int(), z.number().int(), z.number().int()]),
11
+ density: z.array(scalar),
12
+ voxelSize: scalar.positive(),
13
+ color: vector,
14
+ emission: vector,
15
+ densityScale: scalar.nonnegative(),
16
+ })
17
+ .strict();
18
+ export type VolumeData = z.infer<typeof volumeSchema>;
19
+
20
+ export function volumeMesh(data: VolumeData): THREE.Mesh<THREE.BoxGeometry, THREE.ShaderMaterial> {
21
+ const size = data.max.map((v, i) => v - data.min[i]! + 1);
22
+ if (size.some((v) => v <= 0) || size.reduce((a, b) => a * b, 1) !== data.density.length)
23
+ throw new Error('Volume density dimensions do not match the evaluated grid');
24
+ const texture = new THREE.Data3DTexture(
25
+ new Float32Array(data.density),
26
+ size[0]!,
27
+ size[1]!,
28
+ size[2]!,
29
+ );
30
+ texture.format = THREE.RedFormat;
31
+ texture.type = THREE.FloatType;
32
+ texture.minFilter = texture.magFilter = THREE.LinearFilter;
33
+ texture.unpackAlignment = 1;
34
+ texture.needsUpdate = true;
35
+ const minimum = new THREE.Vector3(...data.min).addScalar(-0.5).multiplyScalar(data.voxelSize);
36
+ const extent = new THREE.Vector3(size[0], size[1], size[2]).multiplyScalar(data.voxelSize);
37
+ const geometry = new THREE.BoxGeometry(extent.x, extent.y, extent.z);
38
+ const positions = geometry.getAttribute('position');
39
+ for (let i = 0; i < positions.count; i++)
40
+ positions.setXYZ(
41
+ i,
42
+ minimum.x + (positions.getX(i) > 0 ? extent.x : 0),
43
+ minimum.y + (positions.getY(i) > 0 ? extent.y : 0),
44
+ minimum.z + (positions.getZ(i) > 0 ? extent.z : 0),
45
+ );
46
+ positions.needsUpdate = true;
47
+ geometry.computeBoundingBox();
48
+ geometry.computeBoundingSphere();
49
+ const material = new THREE.ShaderMaterial({
50
+ glslVersion: THREE.GLSL3,
51
+ side: THREE.BackSide,
52
+ transparent: true,
53
+ depthWrite: false,
54
+ premultipliedAlpha: true,
55
+ uniforms: {
56
+ densityGrid: { value: texture },
57
+ minimum: { value: minimum },
58
+ extent: { value: extent },
59
+ voxelSize: { value: data.voxelSize },
60
+ densityScale: { value: data.densityScale },
61
+ volumeColor: { value: new THREE.Vector3(...data.color) },
62
+ emission: { value: new THREE.Vector3(...data.emission) },
63
+ cameraLocal: { value: new THREE.Vector3() },
64
+ rayLocal: { value: new THREE.Vector3() },
65
+ orthographic: { value: false },
66
+ worldScale: { value: new THREE.Matrix3() },
67
+ },
68
+ vertexShader: `
69
+ out vec3 localPosition;
70
+ void main() {
71
+ localPosition = position;
72
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
73
+ }`,
74
+ fragmentShader: `
75
+ precision highp sampler3D;
76
+ uniform sampler3D densityGrid;
77
+ uniform vec3 minimum, extent, volumeColor, emission, cameraLocal, rayLocal;
78
+ uniform float voxelSize, densityScale;
79
+ uniform bool orthographic;
80
+ uniform mat3 worldScale;
81
+ in vec3 localPosition;
82
+ out vec4 outputColor;
83
+ #define gl_FragColor outputColor
84
+ void main() {
85
+ vec3 direction = normalize(orthographic ? rayLocal : localPosition - cameraLocal);
86
+ vec3 origin = orthographic ? localPosition - direction * length(extent) * 2.0 : cameraLocal;
87
+ vec3 inverseRay = 1.0 / direction;
88
+ vec3 nearPlane = (minimum - origin) * inverseRay;
89
+ vec3 farPlane = (minimum + extent - origin) * inverseRay;
90
+ vec3 lo = min(nearPlane, farPlane), hi = max(nearPlane, farPlane);
91
+ float start = max(0.0, max(lo.x, max(lo.y, lo.z)));
92
+ float end = min(hi.x, min(hi.y, hi.z));
93
+ if (end <= start) discard;
94
+ float stepSize = min(voxelSize * 0.5, end - start);
95
+ float worldStep = length(worldScale * direction) * stepSize;
96
+ vec3 radiance = vec3(0.0);
97
+ float transmission = 1.0;
98
+ for (float t = start + stepSize * 0.5; t < end; t += stepSize) {
99
+ vec3 uvw = (origin + direction * t - minimum) / extent;
100
+ float density = max(0.0, texture(densityGrid, uvw).r);
101
+ float extinction = density * densityScale;
102
+ float attenuation = exp(-extinction * worldStep);
103
+ float integral = extinction > 0.000001 ? (1.0 - attenuation) / extinction : worldStep;
104
+ radiance += transmission * (volumeColor * extinction + emission * density) * integral;
105
+ transmission *= attenuation;
106
+ if (transmission < 0.001) break;
107
+ }
108
+ outputColor = vec4(radiance, 1.0 - transmission);
109
+ #include <tonemapping_fragment>
110
+ #include <colorspace_fragment>
111
+ }`,
112
+ });
113
+ const mesh = new THREE.Mesh(geometry, material);
114
+ const inverse = new THREE.Matrix4();
115
+ mesh.onBeforeRender = (_renderer, _scene, camera) => {
116
+ inverse.copy(mesh.matrixWorld).invert();
117
+ material.uniforms['cameraLocal']!.value.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(
118
+ inverse,
119
+ );
120
+ camera.getWorldDirection(material.uniforms['rayLocal']!.value).transformDirection(inverse);
121
+ material.uniforms['orthographic']!.value = Boolean(
122
+ (camera as THREE.OrthographicCamera).isOrthographicCamera,
123
+ );
124
+ material.uniforms['worldScale']!.value.setFromMatrix4(mesh.matrixWorld);
125
+ };
126
+ material.addEventListener('dispose', () => texture.dispose());
127
+ return mesh;
128
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * THE WEIGHT OVERLAY — the active vertex group's weights, coloured with
3
+ * Blender's own ramp (ARCHITECTURE-CORE §Blender north star, "Inspection
4
+ * parity, not editing parity" and "The reference is Blender's SOURCE as well
5
+ * as its frames"; WORK.md §Blender in the tab is Blender, "Inspection parity",
6
+ * I4).
7
+ *
8
+ * IT IS INSPECTION, NOT PAINT MODE. Blender shows these colours by entering
9
+ * Weight Paint; here it is a viewport overlay toggle beside Bones, because the
10
+ * Model document has no brushes to enter a paint mode for and looking at the
11
+ * weights is the whole point.
12
+ *
13
+ * WHICH RAMP, and the correction it cost. The brief for this unit named
14
+ * `BKE_defvert_weight_to_rgb` / `weight_to_rgb`
15
+ * (`blenkernel/intern/deform.cc:1559-1590`) — the blue → cyan → green →
16
+ * yellow → red piecewise ramp. MEASURED at the engine's pin, that function is
17
+ * NOT what paints a vertex group: its only callers in the whole tree are
18
+ * `blenkernel/intern/particle.cc:3602,3624-3625`, the HAIR KEY weight colour.
19
+ * The weight-paint viewport samples a 256-texel 1D table built in
20
+ * `draw/engines/overlay/overlay_instance.cc:186-221`, and that table is an HSV
21
+ * sweep with a gamma correction — a different curve with the same endpoints,
22
+ * which is why the two look alike until you compare mid-tones. This file is
23
+ * the table Blender actually samples; the piecewise ramp is named here so the
24
+ * next reader does not "fix" it back.
25
+ */
26
+ import * as THREE from 'three';
27
+ import { z } from 'zod';
28
+
29
+ /** What `session.py`'s `_weights` answers. `weightsBase64` / `alertBase64` are
30
+ * absent on a REFERENCE (`unchanged`), which is the same contract the meshes
31
+ * have (`blender-runtime-frame.ts`): the array is the size of a vertex column
32
+ * and every mutation presents. */
33
+ export const weightsSchema = z
34
+ .object({
35
+ object: z.string(),
36
+ group: z.string(),
37
+ groupIndex: z.number().int().nonnegative(),
38
+ count: z.number().int().nonnegative(),
39
+ /** sha1 of the packed weight + alert bytes. The reference key, and the
40
+ * presenter's staleness signal — never the mesh's revision, which does
41
+ * NOT move when only deform weights change (`session.py::_weights` states
42
+ * the measurement). */
43
+ digest: z.string(),
44
+ /** `scene.tool_settings.vertex_group_user` (`rna_scene.cc:3428-3434`). */
45
+ alertMode: z.enum(['NONE', 'ACTIVE', 'ALL']),
46
+ unchanged: z.literal(true).optional(),
47
+ /** Float32, one per vertex, clamped to [0,1]. */
48
+ weightsBase64: z.string().optional(),
49
+ /** Uint8, one per vertex: 1 where Blender paints the unreferenced colour. */
50
+ alertBase64: z.string().optional(),
51
+ })
52
+ .strict();
53
+
54
+ export type BlenderWeights = z.infer<typeof weightsSchema>;
55
+
56
+ /**
57
+ * BLENDER'S WEIGHT RAMP, the formula from `overlay_instance.cc:186-203`.
58
+ *
59
+ * hsv = { (2/3)·(1 − weight), 1, (0.5 + 0.5·weight)^γ }, γ = 1.5
60
+ * rgb = hsv_to_rgb(hsv)
61
+ * rgb = rgb^(1/γ)
62
+ *
63
+ * The comment there states the intent: "Use gamma correction to even out the
64
+ * color bands: increasing widens yellow/cyan vs red/green/blue. Gamma 1.0
65
+ * produces the original 2.79 color ramp."
66
+ *
67
+ * The STOPS the hue sweep passes through, at γ = 1.5 and rounded to a byte —
68
+ * the values this function returns, which is what a frame can be read against:
69
+ *
70
+ * 0.00 hue 240° #000080 blue (V = 0.5^1.5 = 0.3536 → ^(1/1.5) = 0.500)
71
+ * 0.25 hue 180° #009f9f cyan (V = 0.625^1.5 → 0.625)
72
+ * 0.50 hue 120° #00bf00 green (V = 0.75^1.5 → 0.750)
73
+ * 0.75 hue 60° #dfdf00 yellow (V = 0.875^1.5 → 0.875)
74
+ * 1.00 hue 0° #ff0000 red (V = 1 → 1.000)
75
+ *
76
+ * The channel value at each stop is `((0.5 + 0.5·w)^1.5)^(1/1.5)`, which is
77
+ * just `0.5 + 0.5·w` — the gamma cancels on a fully saturated channel and only
78
+ * bends the MIXED ones between the stops, which is exactly the "even out the
79
+ * color bands" the comment claims. So the five stops above are 0.5, 0.625,
80
+ * 0.75, 0.875, 1.0 of full, and any reading of a frame can be checked against
81
+ * those five bytes: 0x80, 0x9f, 0xbf, 0xdf, 0xff.
82
+ *
83
+ * THE TABLE IS AN sRGB TEXTURE in Blender (`ensure_1d(TextureFormat::
84
+ * SRGBA_8_8_8_8, 256, …)`, `:219-220`), so the sampler linearises it on read
85
+ * and the values above are the sRGB bytes. The colours built here are handed
86
+ * to three the same way — read as sRGB, so the pixel matches.
87
+ */
88
+ export function weightColor(weight: number): THREE.Color {
89
+ const gamma = 1.5;
90
+ const clamped = weight < 0 ? 0 : weight > 1 ? 1 : weight;
91
+ const value = (0.5 + 0.5 * clamped) ** gamma;
92
+ // HSV, not HSL: `hsv_to_rgb_v` with saturation 1 is the hue's own unit ramp
93
+ // scaled by V, which three's `setHSL` cannot express — so the sweep is done
94
+ // here and only the colour space is three's.
95
+ const [r, g, b] = hueToRgb((2 / 3) * (1 - clamped));
96
+ const color = new THREE.Color();
97
+ color.setRGB(
98
+ (r * value) ** (1 / gamma),
99
+ (g * value) ** (1 / gamma),
100
+ (b * value) ** (1 / gamma),
101
+ THREE.SRGBColorSpace,
102
+ );
103
+ return color;
104
+ }
105
+
106
+ /** `hsv_to_rgb` with saturation 1 and value 1 — the six-sector hue ramp
107
+ * (`BLI_math_color.c`'s own sector table, reduced to the s=v=1 case Blender's
108
+ * weight table is the only caller of). */
109
+ function hueToRgb(hue: number): [number, number, number] {
110
+ const h = ((hue % 1) + 1) % 1;
111
+ const i = Math.floor(h * 6);
112
+ const f = h * 6 - i;
113
+ switch (i % 6) {
114
+ case 0:
115
+ return [1, f, 0];
116
+ case 1:
117
+ return [1 - f, 1, 0];
118
+ case 2:
119
+ return [0, 1, f];
120
+ case 3:
121
+ return [0, 1 - f, 1];
122
+ case 4:
123
+ return [f, 0, 1];
124
+ default:
125
+ return [1, 0, 1 - f];
126
+ }
127
+ }
128
+
129
+ /**
130
+ * `TH_VERTEX_UNREFERENCED`, the colour a zero-weight vertex is painted
131
+ * (`overlay_paint_weight_frag.glsl:98-100`, `mix(weight_color,
132
+ * color_unreferenced, alert * alert)` at alert 1).
133
+ *
134
+ * MEASURED: `vertex_unreferenced` appears NOWHERE in
135
+ * `release/datafiles/userdef/userdef_default_theme.c` — the only mention of
136
+ * the member in the whole checkout is its RNA declaration
137
+ * (`rna_userdef.cc:3104`). The default theme therefore leaves the struct's
138
+ * zero standing, which is BLACK, and black is what Blender draws for an
139
+ * unweighted vertex. Stated rather than eyedropped.
140
+ */
141
+ export const VERTEX_UNREFERENCED = 0x000000;
142
+
143
+ function bytesFrom(base64: string): Uint8Array {
144
+ const binary = atob(base64);
145
+ const bytes = new Uint8Array(binary.length);
146
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
147
+ return bytes;
148
+ }
149
+
150
+ /**
151
+ * THE OVERLAY: the painted object's own geometry, drawn again in the weight
152
+ * colours.
153
+ *
154
+ * A SECOND MESH rather than a material swap on the presented one, and the
155
+ * reason is ownership: the presented mesh belongs to `BlenderRuntimeView`,
156
+ * which replaces its geometry and material whenever the engine says so, and a
157
+ * toggle that reached in and swapped the material would be fighting it every
158
+ * frame. The overlay holds its own mesh over the same geometry, one polygon
159
+ * offset in front, and the Helpers toggle turns it on and off the way it turns
160
+ * every other helper on and off.
161
+ */
162
+ export class WeightOverlay {
163
+ readonly group = new THREE.Group();
164
+ private mesh: THREE.Mesh | null = null;
165
+ private material: THREE.MeshBasicMaterial | null = null;
166
+ private geometry: THREE.BufferGeometry | null = null;
167
+ /** The last array that crossed, kept because a present ships a REFERENCE for
168
+ * everything it has already sent. Keyed by `<object>:<group>`. */
169
+ private held: { key: string; weights: Float32Array; alerts: Uint8Array } | null = null;
170
+ private signature = '';
171
+
172
+ constructor() {
173
+ this.group.name = 'BlenderWeightOverlay';
174
+ }
175
+
176
+ /**
177
+ * Draw the weights over `source`, the presented mesh of the painted object.
178
+ * `null` weights (no active vertex group, no active mesh) clears it.
179
+ *
180
+ * `objectMatrix` is the object's matrix in BLENDER's frame — the frame's own
181
+ * `objects[].matrix`, exactly as the armature overlay takes it, and NOT the
182
+ * presented mesh's `matrixWorld`. Measured live 2026-09-19: `matrixWorld`
183
+ * already carries the model root's Z-up permutation, and this group carries
184
+ * it too, so the drawing landed with the permutation applied TWICE — the
185
+ * Body's colours lay flat on the floor beside the standing cylinder.
186
+ *
187
+ * Returns the warning this drawing could not honour, or null.
188
+ */
189
+ apply(
190
+ weights: BlenderWeights | null,
191
+ source: THREE.Mesh | null,
192
+ objectMatrix: THREE.Matrix4 | null,
193
+ ): string | null {
194
+ if (weights === null || source === null || !source.isMesh || objectMatrix === null) {
195
+ // THE DRAWING GOES, THE ARRAY STAYS. Dropping `held` here is what the
196
+ // walk caught (2026-09-19): `_weights` answers null whenever the active
197
+ // object is not a mesh — entering POSE MODE on the rig is enough — while
198
+ // the session's `_known` still records the array as sent, so the next
199
+ // frame that names the Body again ships a REFERENCE to bytes this side
200
+ // had just thrown away, and the colours never came back. The array is
201
+ // still the truth about that object's group; only the drawing is stale.
202
+ // It is the same rule the meshes follow: a presenter holds its geometry
203
+ // until the SESSION replaces it, never until a frame stops mentioning it.
204
+ this.clear();
205
+ this.signature = '';
206
+ return null;
207
+ }
208
+ const key = `${weights.object}:${weights.group}`;
209
+ if (weights.unchanged === true) {
210
+ if (this.held?.key !== key) {
211
+ // The session's record is ahead of this overlay — the document was
212
+ // rebuilt under a still-running session. Named, and answered by the
213
+ // next present: the weight array is re-sent whenever the mesh moves.
214
+ this.clear();
215
+ return (
216
+ `weights: the session sent a reference to ${key} that this overlay does not hold; ` +
217
+ 'the colours reappear on the next change to the mesh or the group'
218
+ );
219
+ }
220
+ } else {
221
+ if (weights.weightsBase64 === undefined || weights.alertBase64 === undefined) {
222
+ this.clear();
223
+ return `weights: ${key} carried neither an array nor a reference`;
224
+ }
225
+ const raw = bytesFrom(weights.weightsBase64);
226
+ this.held = {
227
+ key,
228
+ weights: new Float32Array(raw.buffer, raw.byteOffset, weights.count),
229
+ alerts: bytesFrom(weights.alertBase64),
230
+ };
231
+ }
232
+ const held = this.held;
233
+ if (held === null) return null;
234
+ // WHAT MAKES THE DRAWING STALE: the array, the group, and the GEOMETRY the
235
+ // colours are laid over (the presenter replaces it when the mesh moves).
236
+ const signature = `${key}:${weights.digest}:${source.geometry.uuid}:${weights.alertMode}:${objectMatrix.elements.join(',')}`;
237
+ if (signature === this.signature && this.mesh !== null) return null;
238
+ this.signature = signature;
239
+ this.clear();
240
+ const geometry = source.geometry.clone();
241
+ const position = geometry.getAttribute('position');
242
+ // THE PRESENTED GEOMETRY IS DRAWN-OUT: the draw splits one Blender vertex
243
+ // into as many drawn ones as its corners need (a UV seam, a sharp edge, a
244
+ // flat face), so a per-vertex fact off the engine cannot be laid over it by
245
+ // index. `blenderVertex` is the draw's own map, written by the one function
246
+ // that creates a drawn vertex (`blender-runtime-geometry.ts`, `emit`).
247
+ // Without it the overlay refuses by name rather than colouring by a guess.
248
+ const sourceIndex = geometry.getAttribute('blenderVertex');
249
+ if (sourceIndex === undefined) {
250
+ geometry.dispose();
251
+ return (
252
+ 'weights: the presented geometry carries no per-vertex Blender index ' +
253
+ '(`blenderVertex`), so the weights cannot be laid over it'
254
+ );
255
+ }
256
+ const colors = new Float32Array(position.count * 3);
257
+ const unreferenced = new THREE.Color().setHex(VERTEX_UNREFERENCED, THREE.SRGBColorSpace);
258
+ for (let i = 0; i < position.count; i++) {
259
+ const vertex = sourceIndex.getX(i);
260
+ const alert = weights.alertMode !== 'NONE' && held.alerts[vertex] === 1;
261
+ const color = alert ? unreferenced : weightColor(held.weights[vertex] ?? 0);
262
+ colors[i * 3] = color.r;
263
+ colors[i * 3 + 1] = color.g;
264
+ colors[i * 3 + 2] = color.b;
265
+ }
266
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
267
+ // Read and dropped: a `Uint32Array` attribute nothing's shader declares is
268
+ // dead weight on the upload, and the colours it produced are the drawing.
269
+ geometry.deleteAttribute('blenderVertex');
270
+ const material = new THREE.MeshBasicMaterial({
271
+ vertexColors: true,
272
+ // Blender's weight pass replaces the surface's shading with the ramp and
273
+ // multiplies only by `color_fac`, which is 1 unless Fake Shading is on
274
+ // (`overlay_paint_weight_vert.glsl:20-28`). An unlit material IS that.
275
+ polygonOffset: true,
276
+ polygonOffsetFactor: -1,
277
+ polygonOffsetUnits: -1,
278
+ });
279
+ const mesh = new THREE.Mesh(geometry, material);
280
+ mesh.name = `${weights.object}:weights`;
281
+ mesh.frustumCulled = false;
282
+ mesh.matrixAutoUpdate = false;
283
+ mesh.matrix.copy(objectMatrix);
284
+ mesh.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale);
285
+ this.geometry = geometry;
286
+ this.material = material;
287
+ this.mesh = mesh;
288
+ this.group.add(mesh);
289
+ return null;
290
+ }
291
+
292
+ private clear(): void {
293
+ if (this.mesh) this.group.remove(this.mesh);
294
+ this.geometry?.dispose();
295
+ this.material?.dispose();
296
+ this.mesh = null;
297
+ this.geometry = null;
298
+ this.material = null;
299
+ }
300
+
301
+ dispose(): void {
302
+ this.clear();
303
+ this.held = null;
304
+ this.signature = '';
305
+ }
306
+ }