arkai2025 Claude Opus 4.8 (1M context) commited on
Commit
f88d0cb
·
1 Parent(s): 3b3e73a

feat(voxel): ACES tone mapping, studio rig, contrast-aware bg + contact shadow

Browse files
Files changed (1) hide show
  1. src/buddy_fusion/voxel_embed.py +73 -17
src/buddy_fusion/voxel_embed.py CHANGED
@@ -283,29 +283,63 @@ function voxelThumbRenderer(THREE) {
283
  if (!window.__voxelThumbRenderer) {
284
  const r = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
285
  r.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
 
 
 
286
  window.__voxelThumbRenderer = r;
287
  }
288
  return window.__voxelThumbRenderer;
289
  }
290
 
291
- // Crossy-Road style daytime sky: a vertical gradient (deep blue up top, light at
292
- // the horizon) baked into a CanvasTexture. Assigned to scene.background so the
293
- // shared offscreen thumbnail renderer captures the SAME sky as live stages — a
294
- // CSS gradient on the stage div would not appear in the toDataURL snapshot.
295
- function voxelSkyTexture(THREE) {
296
- if (!window.__voxelSky) {
297
- const c = document.createElement("canvas");
298
- c.width = 4; c.height = 256;
299
- const ctx = c.getContext("2d");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  const g = ctx.createLinearGradient(0, 0, 0, 256);
301
- g.addColorStop(0, "#62b6f0"); // top
302
- g.addColorStop(1, "#bfe6ff"); // horizon
303
  ctx.fillStyle = g; ctx.fillRect(0, 0, 4, 256);
304
- const tex = new THREE.CanvasTexture(c);
305
  if (THREE.SRGBColorSpace) tex.colorSpace = THREE.SRGBColorSpace;
306
- window.__voxelSky = tex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  }
308
- return window.__voxelSky;
309
  }
310
 
311
 
@@ -325,10 +359,13 @@ async function setupVoxel(root) {
325
  const THREE = await voxelLoadThree();
326
  const W = stage.clientWidth || 320, H = stage.clientHeight || 300;
327
  const scene = new THREE.Scene();
328
- scene.background = voxelSkyTexture(THREE);
329
  const cam = new THREE.PerspectiveCamera(42, W/H, 0.1, 100);
330
- scene.add(new THREE.HemisphereLight(0xffffff, 0xbfe6ff, 1.05)); // soft sky bounce
331
- const key = new THREE.DirectionalLight(0xffffff, 1.3); key.position.set(6, 12, 8); scene.add(key);
 
 
 
 
332
 
333
  // Just the creature against the gradient sky background — no ground, podium, or
334
  // props. The renderer still accepts a multi-creature config (cfg.creatures),
@@ -339,6 +376,7 @@ async function setupVoxel(root) {
339
  const sceneBox = new THREE.Box3();
340
  genomes.forEach((g, i) => {
341
  const cells = (cfg.boxes && cfg.boxes.length) ? voxelsFromBoxes(cfg.boxes) : voxelsFromGenome(g);
 
342
  const mesh = voxelBuildMesh(THREE, cells);
343
  const bb = new THREE.Box3().setFromObject(mesh);
344
  const c = bb.getCenter(new THREE.Vector3());
@@ -351,10 +389,25 @@ async function setupVoxel(root) {
351
  pivot.add(mesh);
352
  scene.add(pivot);
353
  pivots.push(pivot);
 
 
 
 
 
 
 
 
 
 
 
354
  sceneBox.expandByPoint(new THREE.Vector3(offX - s.x / 2, c.y - s.y / 2, -s.z / 2));
355
  sceneBox.expandByPoint(new THREE.Vector3(offX + s.x / 2, c.y + s.y / 2, s.z / 2));
356
  });
357
 
 
 
 
 
358
  const sCenter = sceneBox.getCenter(new THREE.Vector3());
359
  const radius = sceneBox.getSize(new THREE.Vector3()).length() * 0.7 + 3;
360
 
@@ -383,6 +436,9 @@ async function setupVoxel(root) {
383
 
384
  const renderer = new THREE.WebGLRenderer({ antialias: true });
385
  renderer.setSize(W, H); renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
 
 
 
386
  stage.appendChild(renderer.domElement);
387
  const { OrbitControls } = await voxelLoadOC();
388
  const controls = new OrbitControls(cam, renderer.domElement);
 
283
  if (!window.__voxelThumbRenderer) {
284
  const r = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
285
  r.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
286
+ r.toneMapping = THREE.ACESFilmicToneMapping;
287
+ r.toneMappingExposure = 1.0;
288
+ if (THREE.SRGBColorSpace) r.outputColorSpace = THREE.SRGBColorSpace;
289
  window.__voxelThumbRenderer = r;
290
  }
291
  return window.__voxelThumbRenderer;
292
  }
293
 
294
+
295
+ // Dominant hue of the creature -> a soft pastel background hue rotated AWAY from
296
+ // it, so the body never sits on a same-hue wash (e.g. a red body never gets a red
297
+ // bg). Low saturation, high lightness; vertical gradient. Cached by hue bucket.
298
+ function voxelDominantHueDeg(THREE, cells) {
299
+ const tally = new Map();
300
+ for (const cell of cells) tally.set(cell.color, (tally.get(cell.color) || 0) + 1);
301
+ let best = null, bestN = -1;
302
+ for (const [hex, n] of tally) { if (n > bestN) { bestN = n; best = hex; } }
303
+ const c = new THREE.Color(); c.set(best || "#caa06f");
304
+ const hsl = {}; c.getHSL(hsl);
305
+ return hsl.h * 360;
306
+ }
307
+
308
+ function voxelBgTexture(THREE, cells) {
309
+ const hue = voxelDominantHueDeg(THREE, cells);
310
+ const bgHue = ((hue + 150) % 360) / 360; // rotate away from the body hue
311
+ const bucket = Math.round(bgHue * 24); // quantise for caching
312
+ window.__voxelBg = window.__voxelBg || {};
313
+ if (!window.__voxelBg[bucket]) {
314
+ const top = new THREE.Color().setHSL(bgHue, 0.32, 0.90);
315
+ const bot = new THREE.Color().setHSL(bgHue, 0.38, 0.80);
316
+ const cv = document.createElement("canvas"); cv.width = 4; cv.height = 256;
317
+ const ctx = cv.getContext("2d");
318
  const g = ctx.createLinearGradient(0, 0, 0, 256);
319
+ g.addColorStop(0, "#" + top.getHexString());
320
+ g.addColorStop(1, "#" + bot.getHexString());
321
  ctx.fillStyle = g; ctx.fillRect(0, 0, 4, 256);
322
+ const tex = new THREE.CanvasTexture(cv);
323
  if (THREE.SRGBColorSpace) tex.colorSpace = THREE.SRGBColorSpace;
324
+ window.__voxelBg[bucket] = tex;
325
+ }
326
+ return window.__voxelBg[bucket];
327
+ }
328
+
329
+ // A soft round contact-shadow blob (dark centre -> transparent edge). Cheaper
330
+ // than a shadow map and matches the reference (no visible ground, just a blob).
331
+ function voxelShadowTexture(THREE) {
332
+ if (!window.__voxelShadowTex) {
333
+ const cv = document.createElement("canvas"); cv.width = 128; cv.height = 128;
334
+ const ctx = cv.getContext("2d");
335
+ const g = ctx.createRadialGradient(64, 64, 4, 64, 64, 62);
336
+ g.addColorStop(0, "rgba(0,0,0,0.42)");
337
+ g.addColorStop(0.6, "rgba(0,0,0,0.20)");
338
+ g.addColorStop(1, "rgba(0,0,0,0)");
339
+ ctx.fillStyle = g; ctx.fillRect(0, 0, 128, 128);
340
+ window.__voxelShadowTex = new THREE.CanvasTexture(cv);
341
  }
342
+ return window.__voxelShadowTex;
343
  }
344
 
345
 
 
359
  const THREE = await voxelLoadThree();
360
  const W = stage.clientWidth || 320, H = stage.clientHeight || 300;
361
  const scene = new THREE.Scene();
 
362
  const cam = new THREE.PerspectiveCamera(42, W/H, 0.1, 100);
363
+ // Studio rig: warm key from upper-front, cool fill from the opposite lower
364
+ // side, gentle hemisphere bounce. Tuned for ACES tone mapping (set on renderer).
365
+ scene.add(new THREE.HemisphereLight(0xffffff, 0xcdb89a, 0.55));
366
+ const key = new THREE.DirectionalLight(0xfff2e0, 2.2); key.position.set(6, 12, 8); scene.add(key);
367
+ const fill = new THREE.DirectionalLight(0xbfe6ff, 0.7); fill.position.set(-7, 4, -6); scene.add(fill);
368
+ let __bgCells = null;
369
 
370
  // Just the creature against the gradient sky background — no ground, podium, or
371
  // props. The renderer still accepts a multi-creature config (cfg.creatures),
 
376
  const sceneBox = new THREE.Box3();
377
  genomes.forEach((g, i) => {
378
  const cells = (cfg.boxes && cfg.boxes.length) ? voxelsFromBoxes(cfg.boxes) : voxelsFromGenome(g);
379
+ if (i === 0) __bgCells = cells;
380
  const mesh = voxelBuildMesh(THREE, cells);
381
  const bb = new THREE.Box3().setFromObject(mesh);
382
  const c = bb.getCenter(new THREE.Vector3());
 
389
  pivot.add(mesh);
390
  scene.add(pivot);
391
  pivots.push(pivot);
392
+
393
+ // Soft contact shadow blob at the creature's base. In the scene (not the
394
+ // pivot) so the reveal pop / turntable does not scale or spin it.
395
+ const shadow = new THREE.Mesh(
396
+ new THREE.PlaneGeometry(1, 1),
397
+ new THREE.MeshBasicMaterial({ map: voxelShadowTexture(THREE), transparent: true,
398
+ depthWrite: false, opacity: 0.9 }));
399
+ shadow.rotation.x = -Math.PI / 2;
400
+ shadow.position.set(offX, (c.y - s.y / 2) + 0.02, 0);
401
+ shadow.scale.set(s.x * 1.7 + 1, s.z * 1.7 + 1, 1);
402
+ scene.add(shadow);
403
  sceneBox.expandByPoint(new THREE.Vector3(offX - s.x / 2, c.y - s.y / 2, -s.z / 2));
404
  sceneBox.expandByPoint(new THREE.Vector3(offX + s.x / 2, c.y + s.y / 2, s.z / 2));
405
  });
406
 
407
+ // Per-creature pastel background, hue-rotated away from the body so it frames
408
+ // without colliding (a red body never sits on a red wash).
409
+ scene.background = voxelBgTexture(THREE, __bgCells || []);
410
+
411
  const sCenter = sceneBox.getCenter(new THREE.Vector3());
412
  const radius = sceneBox.getSize(new THREE.Vector3()).length() * 0.7 + 3;
413
 
 
436
 
437
  const renderer = new THREE.WebGLRenderer({ antialias: true });
438
  renderer.setSize(W, H); renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
439
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
440
+ renderer.toneMappingExposure = 1.0;
441
+ if (THREE.SRGBColorSpace) renderer.outputColorSpace = THREE.SRGBColorSpace;
442
  stage.appendChild(renderer.domElement);
443
  const { OrbitControls } = await voxelLoadOC();
444
  const controls = new OrbitControls(cam, renderer.domElement);