feat(voxel): ACES tone mapping, studio rig, contrast-aware bg + contact shadow
Browse files- 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 |
-
|
| 292 |
-
//
|
| 293 |
-
//
|
| 294 |
-
//
|
| 295 |
-
function
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
const g = ctx.createLinearGradient(0, 0, 0, 256);
|
| 301 |
-
g.addColorStop(0, "#
|
| 302 |
-
g.addColorStop(1, "#
|
| 303 |
ctx.fillStyle = g; ctx.fillRect(0, 0, 4, 256);
|
| 304 |
-
const tex = new THREE.CanvasTexture(
|
| 305 |
if (THREE.SRGBColorSpace) tex.colorSpace = THREE.SRGBColorSpace;
|
| 306 |
-
window.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
}
|
| 308 |
-
return window.
|
| 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 |
-
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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);
|