arkai2025 Claude Opus 4.8 (1M context) commited on
Commit
22f2fe2
·
1 Parent(s): c91fc1c

fix: spin voxel creatures around their own center, not the world origin

Browse files

The live turntable rotated `mesh.rotation.y`, but the mesh sits at the
world origin while the camera aims at the bounding-box center. Asymmetric
archetypes (e.g. the horse, whose long body + front-protruding head puts
its centroid at z~2) orbited the origin as they spun and slid off-frame —
visibly jamming the horse against the right edge of its stage.

Wrap the mesh in a pivot Group at `center` with the mesh offset by
-center, and rotate/scale the pivot in all three render paths (live
turntable, reveal pop, static thumbnail). The creature now spins and
pops in place around its own center.

Also add wiki/voxel-rendering.md documenting the voxelizer, the
bounds-driven camera, the turntable pivot rule, and the thumbnail
shared-renderer context cap; link it from the wiki index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

src/buddy_fusion/voxel_embed.py CHANGED
@@ -198,11 +198,21 @@ async function setupVoxel(root) {
198
  scene.add(new THREE.HemisphereLight(0xffffff, 0x404060, 1.1));
199
  const key = new THREE.DirectionalLight(0xffffff, 1.4); key.position.set(6, 12, 8); scene.add(key);
200
  const cells = voxelsFromGenome(cfg.genome);
201
- const mesh = voxelBuildMesh(THREE, cells); scene.add(mesh);
202
  // frame the whole creature (incl. legs/horns) with padding from its bounds.
203
  const bb = new THREE.Box3().setFromObject(mesh);
204
  const center = bb.getCenter(new THREE.Vector3());
205
  const radius = bb.getSize(new THREE.Vector3()).length() * 0.7 + 3;
 
 
 
 
 
 
 
 
 
 
206
  // 3/4 top-down isometric-ish angle, Crossy-Road style.
207
  cam.position.set(center.x + radius * 0.85, center.y + radius * 0.62, center.z + radius * 0.85);
208
  cam.lookAt(center);
@@ -212,7 +222,7 @@ async function setupVoxel(root) {
212
  // One WebGL context per thumbnail would blow past the browser's ~16-context
213
  // cap as the family tree grows and silently blank the oldest stages; a
214
  // single shared context keeps the total flat regardless of tree size.
215
- mesh.rotation.y = 0.7;
216
  const shared = voxelThumbRenderer(THREE);
217
  shared.setSize(W, H);
218
  shared.render(scene, cam);
@@ -236,9 +246,9 @@ async function setupVoxel(root) {
236
  function loop(now) {
237
  if (!root.isConnected) { renderer.dispose(); if (renderer.forceContextLoss) renderer.forceContextLoss(); return; }
238
  const dt = Math.min(0.05, (now - t0)/1000); t0 = now;
239
- mesh.rotation.y += dt * 0.5;
240
- if (reveal < 1) { reveal = Math.min(1, reveal + dt*1.1); mesh.scale.setScalar(0.15 + 0.85*voxelEaseReveal(reveal)); }
241
- else { mesh.scale.setScalar(1); }
242
  controls.update();
243
  renderer.render(scene, cam);
244
  requestAnimationFrame(loop);
 
198
  scene.add(new THREE.HemisphereLight(0xffffff, 0x404060, 1.1));
199
  const key = new THREE.DirectionalLight(0xffffff, 1.4); key.position.set(6, 12, 8); scene.add(key);
200
  const cells = voxelsFromGenome(cfg.genome);
201
+ const mesh = voxelBuildMesh(THREE, cells);
202
  // frame the whole creature (incl. legs/horns) with padding from its bounds.
203
  const bb = new THREE.Box3().setFromObject(mesh);
204
  const center = bb.getCenter(new THREE.Vector3());
205
  const radius = bb.getSize(new THREE.Vector3()).length() * 0.7 + 3;
206
+ // Spin/scale around the creature's OWN center, not the world origin. Most
207
+ // archetypes are asymmetric in the horizontal plane (the horse's long body +
208
+ // front-protruding head puts its centroid well off z=0); rotating the mesh
209
+ // about the origin would orbit that centroid and slide the creature off-frame.
210
+ // A pivot at `center` with the mesh offset by -center keeps it centered.
211
+ const pivot = new THREE.Group();
212
+ mesh.position.sub(center);
213
+ pivot.position.copy(center);
214
+ pivot.add(mesh);
215
+ scene.add(pivot);
216
  // 3/4 top-down isometric-ish angle, Crossy-Road style.
217
  cam.position.set(center.x + radius * 0.85, center.y + radius * 0.62, center.z + radius * 0.85);
218
  cam.lookAt(center);
 
222
  // One WebGL context per thumbnail would blow past the browser's ~16-context
223
  // cap as the family tree grows and silently blank the oldest stages; a
224
  // single shared context keeps the total flat regardless of tree size.
225
+ pivot.rotation.y = 0.7;
226
  const shared = voxelThumbRenderer(THREE);
227
  shared.setSize(W, H);
228
  shared.render(scene, cam);
 
246
  function loop(now) {
247
  if (!root.isConnected) { renderer.dispose(); if (renderer.forceContextLoss) renderer.forceContextLoss(); return; }
248
  const dt = Math.min(0.05, (now - t0)/1000); t0 = now;
249
+ pivot.rotation.y += dt * 0.5;
250
+ if (reveal < 1) { reveal = Math.min(1, reveal + dt*1.1); pivot.scale.setScalar(0.15 + 0.85*voxelEaseReveal(reveal)); }
251
+ else { pivot.scale.setScalar(1); }
252
  controls.update();
253
  renderer.render(scene, cam);
254
  requestAnimationFrame(loop);
wiki/index.md CHANGED
@@ -16,6 +16,8 @@ See [README](README.md) for how this wiki works.
16
  design bet, components, data flow, deployment, and hard-won gotchas.
17
  - [local-dev-runtimes](local-dev-runtimes.md) - Fake vs real runtime boundary,
18
  which mode exercises what, and the local CPU caveat.
 
 
19
 
20
  ## Integrations
21
 
 
16
  design bet, components, data flow, deployment, and hard-won gotchas.
17
  - [local-dev-runtimes](local-dev-runtimes.md) - Fake vs real runtime boundary,
18
  which mode exercises what, and the local CPU caveat.
19
+ - [voxel-rendering](voxel-rendering.md) - The Three.js voxelizer: recipe→cells,
20
+ bounds-driven camera, turntable/reveal pivot, and the thumbnail context cap.
21
 
22
  ## Integrations
23
 
wiki/log.md CHANGED
@@ -2,6 +2,13 @@
2
 
3
  ## 2026-06-14
4
 
 
 
 
 
 
 
 
5
  ### Ported AI/dev tooling from build-a-buddy-hf
6
 
7
  - Added `CLAUDE.md` (workflow rules: English-only, never-auto-push to the Space,
 
2
 
3
  ## 2026-06-14
4
 
5
+ ### Voxel rendering page + turntable centering fix
6
+
7
+ - Added `voxel-rendering.md` documenting the voxelizer, bounds-driven camera,
8
+ the thumbnail shared-renderer context cap, and the turntable pivot rule.
9
+ - Recorded the fix where the turntable rotated about the world origin instead of
10
+ the creature's centroid, sliding asymmetric archetypes (horse) off-frame.
11
+
12
  ### Ported AI/dev tooling from build-a-buddy-hf
13
 
14
  - Added `CLAUDE.md` (workflow rules: English-only, never-auto-push to the Space,
wiki/voxel-rendering.md ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Voxel Rendering (the Three.js voxelizer)
3
+ tags: [architecture, debugging, frontend, three-js, rendering]
4
+ last_updated: 2026-06-14
5
+ confidence: high
6
+ sources:
7
+ - src/buddy_fusion/voxel_embed.py
8
+ related_pages:
9
+ - gradio-embed-integration.md
10
+ - system-overview.md
11
+ ---
12
+
13
+ # Voxel Rendering (the Three.js voxelizer)
14
+
15
+ ## Summary
16
+
17
+ How a genome becomes the cube creature on screen, and the rendering invariants
18
+ that keep it readable and centered. The voxelizer lives entirely in
19
+ `VOXEL_SETUP_JS` inside `src/buddy_fusion/voxel_embed.py` (delivered via
20
+ `js_on_load`; see [gradio-embed-integration](gradio-embed-integration.md) for how
21
+ the embed boots and re-inits). Read this before touching archetype geometry, the
22
+ camera, the turntable, or the reveal animation.
23
+
24
+ ## Key Details
25
+
26
+ - **Recipe → geometry, never raw voxels.** The model only picks the recipe
27
+ (`archetype` + `parts` + `palette`). `voxelsFromGenome(g)` turns that into a
28
+ flat list of `{x, y, z, color}` cells. `VOXEL_ARCH` holds the per-archetype
29
+ body-plan templates (body/legs/head dims, `headMode`, `feat`); `g.parts` layer
30
+ extra/mutated features (leg/horn/wing/tail/arm/eye/second-head) on top.
31
+ - **Cell coordinate convention.** `box(cx,cy,cz,w,h,d,color)` centers a block on
32
+ `(cx, cz)` in the horizontal plane (`x - (w-1)/2`, `z - (d-1)/2`) but grows
33
+ **up** from `cy` (`cy + y`, no vertical centering) — y=0 is the ground, legs
34
+ start there, `bodyY = legLen`. Keep this convention when adding parts or the
35
+ feature will float / sink.
36
+ - **One InstancedMesh per creature.** All cells render as instances of a single
37
+ `BoxGeometry(0.96³)` + `MeshStandardMaterial` (the 0.96 leaves a seam so cubes
38
+ read as discrete). Per-instance color via `setColorAt`. This keeps a creature
39
+ to one draw call regardless of voxel count.
40
+ - **Camera framing is bounds-driven.** After building the mesh, a
41
+ `Box3.setFromObject` gives `center` + `radius`; the camera sits at a fixed 3/4
42
+ top-down isometric offset (`center + radius·(0.85, 0.62, 0.85)`) and
43
+ `lookAt(center)`. So the framing auto-fits any creature size without per-archetype
44
+ tuning.
45
+ - **Reveal animation.** `voxelEaseReveal` is an ease-out-back (overshoots past
46
+ 1.0 then settles) driving `scale` from 0.15→1 for the "pop" when a spliced
47
+ child appears (`autoplay_reveal=True`).
48
+
49
+ ## Failure Modes
50
+
51
+ - **Turntable must spin around the creature's own center, not the world origin.**
52
+ The bug (fixed 2026-06-14): the live stages rotate the creature as a turntable.
53
+ Rotating `mesh.rotation.y` spins about the mesh's local origin, which is the
54
+ **world origin (0,0,0)** — but most archetypes are asymmetric in the
55
+ horizontal plane (e.g. `horse` has `body:[4,4,7]` plus a `headMode:"front"`
56
+ head + snout protruding to z≈7, so its centroid sits around z≈2, well off the
57
+ origin). Spinning an off-origin centroid makes it **orbit** the origin and
58
+ slide out of frame — the horse visibly jammed to the right edge while the more
59
+ symmetric chick stayed put. The camera aims at the fixed initial `center`, so
60
+ the drift is purely the rotation pivot.
61
+ **Fix:** wrap the mesh in a pivot `Group` positioned at `center`, offset the
62
+ mesh by `-center`, and rotate/scale the **pivot** instead of the mesh. The
63
+ geometry then spins (and the reveal scales) in place around its own center.
64
+ Apply it in all three render paths — the live turntable rotation, the reveal
65
+ scale, and the static thumbnail frame (`pivot.rotation.y = 0.7`).
66
+ Detection: watch a tall/long archetype (horse, hippo, dog) through a full
67
+ turn; if it swings toward an edge at some angles instead of rotating in place,
68
+ the pivot is at the origin, not the center.
69
+ - **WebGL context cap (~16) — thumbnails must share one renderer.** Each live
70
+ stage holds a `WebGLRenderer`/context; the family tree grows one node per
71
+ creature, so a live context per thumbnail would blow the browser cap and blank
72
+ the oldest stages. Thumbnails (`voxel_html(thumb=True)`) render through one
73
+ shared offscreen renderer (`window.__voxelThumbRenderer`,
74
+ `preserveDrawingBuffer:true`) and snapshot to an `<img>` — flat context count
75
+ regardless of tree size. Only the two ancestor stages + the child stage keep a
76
+ live context. See [gradio-embed-integration](gradio-embed-integration.md).
77
+ - **Detached live stages leak a context if not disposed.** The animation loop
78
+ guards on `root.isConnected`; when a stage leaves the DOM it calls
79
+ `renderer.dispose()` + `forceContextLoss()` and stops the RAF. Skipping this
80
+ leaks a context per re-render and eventually trips the cap above.
81
+
82
+ ## Sources
83
+
84
+ - `src/buddy_fusion/voxel_embed.py` (`VOXEL_ARCH`, `voxelsFromGenome`,
85
+ `voxelBuildMesh`, `setupVoxel`, the pivot-centering fix, the shared thumbnail
86
+ renderer)
87
+
88
+ ## Related Pages
89
+
90
+ - [gradio-embed-integration](gradio-embed-integration.md)
91
+ - [system-overview](system-overview.md)