@volter/blender-engine 0.1.8 → 0.1.10

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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * BLENDER'S SOLID SHADING, as Blender computes it: `get_world_lighting` from
3
+ * `draw/engines/workbench/shaders/workbench_world_light_lib.glsl`, transcribed line for line,
4
+ * over the four studio lights of the factory `Default` (`SOLID_LIGHTS`, read back from Blender
5
+ * 5.2's preferences with their specular colours) and no ambient (`light_ambient` is zero).
6
+ *
7
+ * Per light: a wrapped diffuse term, `(N·L + wrap) / (1 + wrap)²`; a normalized Blinn specular
8
+ * whose gloss the wrap softens, blended toward a wrapped "environment" term by `wrap²`; a
9
+ * Fresnel lift of the specular colour (`brdf_approx`); and energy conservation that takes the
10
+ * specular's grey level off the diffuse. The surface is the material's VIEWPORT DISPLAY
11
+ * (`Material.diffuse_color`, `roughness`, `metallic`), which is what Solid's MATERIAL colour type
12
+ * reads — never its node graph.
13
+ *
14
+ * WHY A FUNCTION AND NOT A LIGHT RIG: three's lambert and BRDF cannot express the wrap, the
15
+ * coupled specular or the per-light specular colour, so the four lights plus one fitted gain
16
+ * could match the default view's three faces only by absorbing the difference, and missed a
17
+ * face square to the view by 18 levels (143 against Blender's 161). This function, evaluated
18
+ * offline for that face, gives 161.
19
+ *
20
+ * Built on `MeshLambertMaterial` so skinning, morphs and the view-space normal come with it;
21
+ * only the final colour is replaced. Blender uses a fast reciprocal (`fast_rcp`); this uses the
22
+ * exact one.
23
+ */
24
+ import * as THREE from 'three';
25
+ import { SOLID_LIGHTS } from './blender-runtime-lighting';
26
+
27
+ /** What Blender's Solid reads off a material. */
28
+ export interface ViewportDisplay {
29
+ readonly color: readonly [number, number, number, number];
30
+ readonly roughness: number;
31
+ readonly metallic: number;
32
+ }
33
+
34
+ /** For a surface with no material: the factory cube's material's display, read back from Blender
35
+ * 5.2 (grey 0.8, roughness 0.5, metallic 0). Blender's own default surface was not measured. */
36
+ export const DEFAULT_VIEWPORT_DISPLAY: ViewportDisplay = { color: [0.8, 0.8, 0.8, 1], roughness: 0.5, metallic: 0 };
37
+
38
+ const WORKBENCH_GLSL = /* glsl */ `
39
+ uniform vec3 wbDir[4];
40
+ uniform vec3 wbDiffuse[4];
41
+ uniform vec3 wbSpecular[4];
42
+ uniform vec4 wbWrap;
43
+ uniform float wbRoughness;
44
+ uniform float wbMetallic;
45
+
46
+ vec4 wbWrapped(vec4 NL, vec4 w) {
47
+ vec4 w1 = w + 1.0;
48
+ return clamp((NL + w) / (w1 * w1), 0.0, 1.0);
49
+ }
50
+
51
+ vec3 volterWorkbenchLighting(vec3 baseColor, vec3 N, vec3 I) {
52
+ vec3 diffuseColor = mix(baseColor, vec3(0.0), wbMetallic);
53
+ vec3 specularColor = mix(vec3(0.05), baseColor, wbMetallic);
54
+ vec3 R = -reflect(I, N);
55
+ vec4 wrapNL = vec4(dot(wbDir[0], R), dot(wbDir[1], R), dot(wbDir[2], R), dot(wbDir[3], R));
56
+ vec4 specAngle = clamp(vec4(
57
+ dot(normalize(wbDir[0] + I), N), dot(normalize(wbDir[1] + I), N),
58
+ dot(normalize(wbDir[2] + I), N), dot(normalize(wbDir[3] + I), N)), 0.0, 1.0);
59
+ vec4 specNL = clamp(vec4(dot(wbDir[0], N), dot(wbDir[1], N), dot(wbDir[2], N), dot(wbDir[3], N)), 0.0, 1.0);
60
+ vec4 gloss = vec4(1.0 - wbRoughness) * (1.0 - wbWrap);
61
+ vec4 shininess = exp2(10.0 * gloss + 1.0);
62
+ vec4 specLight = pow(specAngle, shininess) * specNL * (shininess * 0.125 + 1.0);
63
+ vec4 envW = mix(wbWrap, vec4(1.0), wbRoughness);
64
+ vec4 specEnv = wbWrapped(wrapNL, envW);
65
+ specLight = mix(specLight, specEnv, wbWrap * wbWrap);
66
+ vec3 specularLight = specLight.x * wbSpecular[0] + specLight.y * wbSpecular[1] +
67
+ specLight.z * wbSpecular[2] + specLight.w * wbSpecular[3];
68
+ float NV = clamp(dot(N, I), 0.0, 1.0);
69
+ float fresnel = exp2(-8.35 * NV) * (1.0 - wbRoughness);
70
+ specularColor = mix(specularColor, vec3(1.0), fresnel);
71
+ specularLight *= specularColor;
72
+ vec4 diffNL = vec4(dot(wbDir[0], N), dot(wbDir[1], N), dot(wbDir[2], N), dot(wbDir[3], N));
73
+ vec4 diffLight = wbWrapped(diffNL, wbWrap);
74
+ vec3 diffuseLight = diffLight.x * wbDiffuse[0] + diffLight.y * wbDiffuse[1] +
75
+ diffLight.z * wbDiffuse[2] + diffLight.w * wbDiffuse[3];
76
+ float specEnergy = dot(specularColor, vec3(0.33333));
77
+ diffuseLight *= diffuseColor * (1.0 - specEnergy);
78
+ return diffuseLight + specularLight;
79
+ }
80
+ `;
81
+
82
+ const vec3s = (pick: (light: (typeof SOLID_LIGHTS)[number]) => readonly number[]) =>
83
+ SOLID_LIGHTS.map((light) => {
84
+ const [x = 0, y = 0, z = 0] = pick(light);
85
+ return new THREE.Vector3(x, y, z);
86
+ });
87
+ const LIGHT_DIRECTIONS = vec3s((light) => light.direction).map((v) => v.normalize());
88
+ const LIGHT_DIFFUSE = vec3s((light) => light.diffuse);
89
+ const LIGHT_SPECULAR = vec3s((light) => light.specular);
90
+ const LIGHT_WRAP = new THREE.Vector4(...SOLID_LIGHTS.map((light) => light.smooth));
91
+
92
+ /** One Solid-shaded surface: Blender's function over this viewport display. */
93
+ export function workbenchMaterial(display: ViewportDisplay, side: THREE.Side): THREE.MeshLambertMaterial {
94
+ const [r, g, b] = display.color;
95
+ const material = new THREE.MeshLambertMaterial({ side });
96
+ material.color.setRGB(r, g, b, THREE.LinearSRGBColorSpace);
97
+ material.name = 'volter:blender-solid';
98
+ material.onBeforeCompile = (shader) => {
99
+ shader.uniforms['wbDir'] = { value: LIGHT_DIRECTIONS };
100
+ shader.uniforms['wbDiffuse'] = { value: LIGHT_DIFFUSE };
101
+ shader.uniforms['wbSpecular'] = { value: LIGHT_SPECULAR };
102
+ shader.uniforms['wbWrap'] = { value: LIGHT_WRAP };
103
+ shader.uniforms['wbRoughness'] = { value: display.roughness };
104
+ shader.uniforms['wbMetallic'] = { value: display.metallic };
105
+ shader.fragmentShader = shader.fragmentShader
106
+ .replace('void main() {', `${WORKBENCH_GLSL}\nvoid main() {`)
107
+ .replace(
108
+ '#include <opaque_fragment>',
109
+ // The view direction toward the eye, as Blender's `I`: constant in an orthographic view.
110
+ 'outgoingLight = volterWorkbenchLighting(diffuseColor.rgb, normal, isOrthographic ? vec3(0.0, 0.0, 1.0) : normalize(vViewPosition));\n#include <opaque_fragment>',
111
+ );
112
+ };
113
+ material.customProgramCacheKey = () => 'volter-blender-workbench';
114
+ return material;
115
+ }
@@ -9,7 +9,7 @@
9
9
  * to the Blender it started:
10
10
  *
11
11
  * const { createPresenter, attachPresenter } = await import(url);
12
- * const detach = attachPresenter(filesystem, "/tmp/vgai-presenter", createPresenter());
12
+ * const detach = attachPresenter(filesystem, "/tmp/volter-presenter", createPresenter());
13
13
  *
14
14
  * NO HOST BUNDLES THIS. The module is GPL because it is part of the Blender
15
15
  * release; a host loads it the way it loads the wasm -- by URL, under a pinned
@@ -3,7 +3,7 @@
3
3
  THE RENDER OVERRIDE IS NOT THE EDITOR'S. `session.py` is the editor tab's
4
4
  long-lived session and carries this same override for the session it owns;
5
5
  this file is the other delivery of it -- a Blender STARTUP module that ships in
6
- the pack's resource tree (`scripts/startup/vgai_three.py`), so a bare
6
+ the pack's resource tree (`scripts/startup/volter_three.py`), so a bare
7
7
  `blender -b ... -a` on ANY surface renders with three.js too. Blender imports
8
8
  every module in `scripts/startup` and calls its `register()`.
9
9
 
@@ -56,14 +56,14 @@ import bpy
56
56
  # `{offset, length, dtype, count, stride}` in a small JSON frame.
57
57
  import _blender_web
58
58
 
59
- ROOT = os.environ.get("VGAI_PRESENTER_ROOT", "/tmp/vgai-presenter")
59
+ ROOT = os.environ.get("VOLTER_PRESENTER_ROOT", "/tmp/volter-presenter")
60
60
  ASK = os.path.join(ROOT, "ask")
61
61
  REPLY = os.path.join(ROOT, "reply")
62
62
  # WHERE THE EXPORT DOOR LEAVES THE ARENA. The host cannot reach this program's
63
63
  # linear memory, so the arena crosses as a file -- the door writes it when
64
64
  # `buffer_path` is set, and the host reads it beside the ask.
65
65
  EXPORT_BUFFER_PATH = os.environ.get(
66
- "VGAI_EXPORT_BUFFER_PATH", os.path.join(ROOT, "arena.bin")
66
+ "VOLTER_EXPORT_BUFFER_PATH", os.path.join(ROOT, "arena.bin")
67
67
  )
68
68
  # THE FRAME'S IDENTITY IS A PAIR, `(session, revision)`, and the presenter
69
69
  # refuses a revision that went backwards under a name it already holds. One
@@ -78,7 +78,7 @@ EXPORT_BUFFER_PATH = os.environ.get(
78
78
  # 2026-09-20 in the substrate tab; the first run on a freshly loaded page
79
79
  # always worked, which is what hid it). The start time is what makes the name
80
80
  # this process's own.
81
- SESSION = "vgai-three:%d-%d" % (os.getpid(), time.time_ns())
81
+ SESSION = "volter-three:%d-%d" % (os.getpid(), time.time_ns())
82
82
  HELLO_TIMEOUT_SECONDS = 2.0
83
83
  POLL_SECONDS = 0.002
84
84
 
@@ -115,14 +115,14 @@ def warn(what):
115
115
  return
116
116
  _WARNED.add(what)
117
117
  _WARNINGS.append(what)
118
- _say("@@VGAI-WARN " + what)
118
+ _say("@@VOLTER-WARN " + what)
119
119
 
120
120
 
121
121
  def _drop(path):
122
122
  try:
123
123
  os.unlink(path)
124
124
  except OSError as error:
125
- _say("@@VGAI-WARN vgai_three could not remove its own %s: %r" % (path, error))
125
+ _say("@@VOLTER-WARN volter_three could not remove its own %s: %r" % (path, error))
126
126
 
127
127
 
128
128
  def _prepare_directories():
@@ -130,7 +130,7 @@ def _prepare_directories():
130
130
  try:
131
131
  os.makedirs(directory, exist_ok=True)
132
132
  except OSError as error:
133
- _say("@@VGAI-ERROR vgai_three cannot create %s: %r" % (directory, error))
133
+ _say("@@VOLTER-ERROR volter_three cannot create %s: %r" % (directory, error))
134
134
  try:
135
135
  os.chmod(directory, 0o777)
136
136
  except OSError:
@@ -371,7 +371,7 @@ def _assert_photographed_from(reported, position, target, up):
371
371
  )
372
372
 
373
373
 
374
- class VgaiRenderEngine(bpy.types.RenderEngine):
374
+ class VolterRenderEngine(bpy.types.RenderEngine):
375
375
  """The scene's renderer, so `write_still`, `save_render` and Render Result
376
376
  behave as Blender's own.
377
377
 
@@ -381,7 +381,7 @@ class VgaiRenderEngine(bpy.types.RenderEngine):
381
381
  starts a path trace nobody asked for.
382
382
  """
383
383
 
384
- bl_idname = "VGAI_THREE"
384
+ bl_idname = "VOLTER_THREE"
385
385
  bl_label = "three.js"
386
386
  bl_use_preview = False
387
387
 
@@ -471,7 +471,7 @@ def _register_engine():
471
471
  ("BLENDER_WORKBENCH", "Workbench"),
472
472
  ):
473
473
  existing = _engine_class(identifier)
474
- if existing is not None and issubclass(existing, VgaiRenderEngine):
474
+ if existing is not None and issubclass(existing, VolterRenderEngine):
475
475
  made.append(identifier)
476
476
  continue
477
477
  if existing is not None:
@@ -482,8 +482,8 @@ def _register_engine():
482
482
  else:
483
483
  _release_from_owning_addon(existing)
484
484
  engine = type(
485
- "Vgai" + identifier.title().replace("_", ""),
486
- (VgaiRenderEngine,),
485
+ "Volter" + identifier.title().replace("_", ""),
486
+ (VolterRenderEngine,),
487
487
  {"bl_idname": identifier, "bl_label": label, "bl_use_preview": False},
488
488
  )
489
489
  try:
@@ -496,17 +496,17 @@ def _register_engine():
496
496
  # keeps `BLENDER_EEVEE` and `BLENDER_WORKBENCH`: a fact about the
497
497
  # build, not a warning on every boot.
498
498
  unavailable.append(identifier)
499
- # `VGAI_THREE` is the engine under its own name, registered ONCE: this runs
499
+ # `VOLTER_THREE` is the engine under its own name, registered ONCE: this runs
500
500
  # again after every file load (the ids have to be retaken), and Blender
501
501
  # refuses a class it already holds -- which is not a capability anyone
502
502
  # lost, so it is not a warning.
503
- if _engine_class(VgaiRenderEngine.bl_idname) is None:
503
+ if _engine_class(VolterRenderEngine.bl_idname) is None:
504
504
  try:
505
- bpy.utils.register_class(VgaiRenderEngine)
505
+ bpy.utils.register_class(VolterRenderEngine)
506
506
  except Exception as error: # noqa: BLE001
507
507
  warn("could not register the three.js engine: %s" % error)
508
- if _engine_class(VgaiRenderEngine.bl_idname) is not None:
509
- made.append(VgaiRenderEngine.bl_idname)
508
+ if _engine_class(VolterRenderEngine.bl_idname) is not None:
509
+ made.append(VolterRenderEngine.bl_idname)
510
510
  return made, unavailable
511
511
 
512
512
 
@@ -518,7 +518,7 @@ def _adopt():
518
518
  _register_engine()
519
519
  for scene in bpy.data.scenes:
520
520
  try:
521
- scene.render.engine = "VGAI_THREE"
521
+ scene.render.engine = "VOLTER_THREE"
522
522
  except (TypeError, AttributeError) as error:
523
523
  warn("scene %s kept its own render engine: %s" % (scene.name, error))
524
524
 
@@ -536,7 +536,7 @@ def register():
536
536
  _prepare_directories()
537
537
  if ask({"op": "hello"}, timeout=HELLO_TIMEOUT_SECONDS) is None:
538
538
  _say(
539
- "vgai: rendering with Cycles — no presenter answered at %s within 2 s"
539
+ "volter: rendering with Cycles — no presenter answered at %s within 2 s"
540
540
  % ROOT
541
541
  )
542
542
  return
package/browser/worker.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  * picks up BETWEEN calls, never mid-call), and carries the bytes to the
28
28
  * project through `/__editor/blender-document` BEFORE acknowledging the edit.
29
29
  *
30
- * WHY THE CARRY IS NOT THE MIRROR'S JOB (`vgai blender-mcp`, class Mirror):
30
+ * WHY THE CARRY IS NOT THE MIRROR'S JOB (`volter blender-mcp`, class Mirror):
31
31
  * the Mirror is pull-based and runs only after an `execute_blender_code`, so
32
32
  * a document saved after the LAST call of a modeling session
33
33
  * would never leave the worker — which is exactly the state this closes
@@ -172,7 +172,7 @@ async function saveDocument(): Promise<void> {
172
172
  throw new Error(`The Blender document ${relative} was not written to the project: ${describeThrown(error)}`);
173
173
  }
174
174
  setDocumentDirty(false);
175
- log('log', `@@VGAI-DOCUMENT ${JSON.stringify({ path: relative, bytes: bytes.length })}`);
175
+ log('log', `@@VOLTER-DOCUMENT ${JSON.stringify({ path: relative, bytes: bytes.length })}`);
176
176
  }
177
177
 
178
178
  async function startBlender(project: string, document?: string): Promise<unknown> {
@@ -286,7 +286,7 @@ async function start(project: string, document?: string): Promise<unknown> {
286
286
  throw new Error(
287
287
  'Headless Blender is not served by this editor, so there is no modeling engine: ' +
288
288
  `${served.missing.join('; ')}. The engine is Blender compiled to WebAssembly ` +
289
- '(packages/blender-engine/wasm, or the directory VGAI_BLENDER_WASM_DIR names); nothing stands in for it.',
289
+ '(packages/blender-engine/wasm, or the directory VOLTER_BLENDER_WASM_DIR names); nothing stands in for it.',
290
290
  );
291
291
  projectRoot = project;
292
292
  return startBlender(project, document);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/blender-engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "GPL-3.0-or-later",
5
- "version": "0.1.8",
5
+ "version": "0.1.10",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },