Code4Scene / cases.html
KoeYe's picture
Cases: show each case's input (prompt, or reference image and instruction) above the outputs
47e9361 verified
Raw History Blame Contribute Delete
32.1 kB
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Code4Scene · Cases in 3D</title><meta name="description" content="Selected Code4Scene cases: every agent&#x27;s saved scene in 3D and the evaluator&#x27;s scores.">
<link rel="icon" href="data:image/svg+xml,<svg viewBox='0 0 32 32' aria-hidden='true'><defs><linearGradient id='gi' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='%2392400e'/><stop offset='1' stop-color='%23d97706'/></linearGradient></defs><rect x='1' y='1' width='30' height='30' rx='8' fill='url(%23gi)'/><path d='M16 6.5 25 11.5v9L16 25.5 7 20.5v-9z' fill='none' stroke='%23fff' stroke-width='2' stroke-linejoin='round'/><path d='M7 11.5 16 16.5 25 11.5M16 16.5v9' fill='none' stroke='%23fff' stroke-width='2' stroke-linejoin='round'/></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=DM+Sans:wght@400;500&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="stylesheet" href="assets/paper.css?v=5427505a">
<style>
:root{--c4s-a:#5a67d8;--c4s-b:#d97706;--c4s-hot:#dc4a2d;--c4s-ok:#0f9f6e;--c4s-g1:#8b93a7;--c4s-g2:#aab1c2;--c4s-g3:#cdd2de;--c4s-seq:#5a67d8;--c4s-unit:#e3e7f0}
svg.ch{width:100%;height:auto;display:block;font-family:Inter,system-ui,sans-serif;overflow:visible}
svg.ch text{fill:var(--text-secondary);font-size:13px}svg.ch .b{fill:var(--text-primary);font-weight:700}svg.ch .m{fill:var(--text-muted);font-size:12px}
svg.ch .num{font-variant-numeric:tabular-nums}svg.ch .grid{stroke:var(--line-soft);stroke-width:1}svg.ch .axis{stroke:var(--line-strong);stroke-width:1}
svg.ch .dim{opacity:.42}svg.ch g.row:hover{opacity:1}svg.ch .hot{fill:var(--c4s-hot)!important;font-weight:700}
.ch-legend{display:flex;flex-wrap:wrap;gap:6px 18px;margin:0 0 10px;font-size:13px;color:var(--text-secondary)}
.ch-legend span{display:inline-flex;align-items:center;gap:7px}.ch-legend i{width:12px;height:12px;border-radius:3px;display:inline-block}
.ev{position:relative;border-radius:var(--radius-sm);overflow:hidden;border:1px solid var(--line-soft)}.ev img{display:block;width:100%}
.ev svg{position:absolute;inset:0;width:100%;height:100%}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}.chip{font-size:13px;font-weight:600;padding:4px 10px;border-radius:999px;border:1px solid var(--line)}
.chip.ok{color:var(--c4s-ok);border-color:currentColor}.chip.no{color:var(--c4s-hot);border-color:currentColor}.chip small{font-weight:500;color:var(--text-muted);margin-left:4px}
.bigstats{display:flex;flex-wrap:wrap;gap:12px 34px;margin:0 0 12px}.bigstats b{display:block;font-size:30px;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.bigstats span{font-size:13px;color:var(--text-secondary)}
</style><style>.casebar{display:grid;gap:10px;margin:0 0 18px}
.cb-row{display:grid;grid-template-columns:140px 1fr;gap:16px;align-items:start}
.cb-row .grp{display:flex;flex-direction:column;line-height:1.25;padding-top:9px}.cb-row .grp b{font-size:13px;color:var(--text-h)}.cb-row .grp span{font-size:12px;color:var(--text-muted)}
.cb-btns{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
.casebar button{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:1px;border:1px solid var(--line);background:var(--surface-1);border-radius:10px;padding:8px 13px;font:600 14px Inter,sans-serif;cursor:pointer;color:var(--text-h);text-align:left;min-width:0}
.casebar button span{line-height:1.3}
.casebar button:hover{border-color:var(--line-strong)}
.casebar button small{font-weight:500;font-size:12px;color:var(--text-muted);white-space:nowrap}
.casebar button.on{background:var(--text-h);color:#fff;border-color:var(--text-h)}.casebar button.on small{color:rgba(255,255,255,.7)}
@media(max-width:900px){.cb-btns{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:600px){.cb-row{grid-template-columns:1fr;gap:6px}.cb-row .grp{flex-direction:row;gap:6px;padding-top:0}}
.casegrid{display:grid;grid-template-columns:minmax(0,1.75fr) minmax(300px,1fr);gap:18px;align-items:start}
.viewer{position:relative;aspect-ratio:16/9;background:#15130f;border-radius:var(--radius-sm);overflow:hidden}
.viewer img,.viewer canvas{width:100%;height:100%;object-fit:cover;display:block}.viewer canvas{cursor:grab}
.viewer .hint{position:absolute;left:12px;bottom:10px;font-size:12px;color:#eee;background:rgba(0,0,0,.5);padding:4px 9px;border-radius:6px;pointer-events:none}
.viewer .load{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#ddd;font-size:14px}
.vtabs{display:flex;gap:6px;margin:10px 0 0;flex-wrap:wrap}
.vtabs button{border:1px solid var(--line);background:var(--surface-1);border-radius:8px;padding:5px 10px;font:600 13px Inter,sans-serif;cursor:pointer;color:var(--text-secondary)}
.vtabs button.on{border-color:var(--text-primary);color:var(--text-primary)}
.agents{display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-top:12px}
.agents .ag{all:unset;cursor:pointer;border-radius:8px;overflow:hidden;outline:2px solid transparent;background:var(--surface-1);border:1px solid var(--line-soft)}
.agents .ag.on{outline-color:var(--accent)}.agents .ag:focus-visible{outline-color:var(--accent-bright)}
.agents .ag .th{aspect-ratio:16/9;background:#222 center/cover no-repeat;position:relative}
.agents .ag .th i{position:absolute;right:4px;top:4px;font:700 10px Inter,sans-serif;background:var(--accent);color:#fff;border-radius:4px;padding:1px 5px;font-style:normal}
.agents .ag .cap{font-size:11px;line-height:1.25;padding:4px 6px 5px;color:var(--text-secondary)}.agents .ag .cap b{font-size:13px;color:var(--text-primary)}
.panel{border:1px solid var(--line-soft);border-radius:var(--radius-sm);padding:14px 16px;margin-bottom:12px;background:var(--surface-1)}
.panel h4{margin:0;font-size:15px;display:flex;align-items:center;gap:7px}.panel .rk{margin:.15rem 0 .4rem;color:var(--text-muted);font-size:13px}
.logo{width:18px;height:18px;display:inline-flex;vertical-align:middle}.logo svg{width:100%;height:100%}
.sc-big{font-size:46px;font-weight:800;letter-spacing:-.03em;font-variant-numeric:tabular-nums;line-height:1.05}
.sc-row{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(60px,1fr) 44px;gap:10px;align-items:center;margin-top:9px;font-size:14px}
.sc-row .tr{height:9px;background:var(--surface-3);border-radius:5px;overflow:hidden}.sc-row .fi{height:100%;background:var(--accent);border-radius:0 4px 4px 0}
.sc-row .v{text-align:right;font-weight:700;font-variant-numeric:tabular-nums}.sc-row .w{color:var(--text-muted);font-size:12px}
.formula{font-size:13px;color:var(--text-muted);margin-top:10px;font-variant-numeric:tabular-nums}
.find{font-size:14px;margin:7px 0;display:flex;gap:8px}.find .m{font-weight:800;width:16px;flex:none}
.find.match .m{color:var(--good)}.find.mismatch .m{color:var(--bad)}.find.physics .m{color:var(--warn)}
.prompt{font-size:14px;max-height:220px;overflow:auto;color:var(--text-secondary);margin-top:6px}
table.cs{width:100%;border-collapse:collapse;font-size:14px;font-variant-numeric:tabular-nums}
table.cs th{text-align:left;font-size:12px;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted);font-weight:700;padding:8px 10px;border-bottom:1px solid var(--line)}
table.cs td{padding:8px 10px;border-bottom:1px solid var(--line-soft)}table.cs tr[data-i]{cursor:pointer}table.cs tr[data-i]:hover{background:var(--surface-2)}
table.cs tr.on{background:var(--accent-wash)}table.cs td.n{color:var(--text-muted);width:32px}table.cs td.nm{font-weight:600;white-space:nowrap}
.cellbar{position:relative;display:flex;align-items:center;gap:8px;min-width:150px}.cellbar div{height:9px;border-radius:0 4px 4px 0;background:var(--accent);max-width:110px;flex:none}
.cellbar b{font-weight:700}
@media(max-width:900px){.casegrid{grid-template-columns:1fr}.agents{grid-template-columns:repeat(4,1fr)}}
.case-head h3{font-size:1.25rem;font-weight:800;color:var(--text-h);letter-spacing:-.01em}.case-head p{font-size:.88rem;color:var(--text-muted);margin:.1rem 0 1rem}
.case-head b{color:var(--primary-dark)}
table.cs td,table.cs th{white-space:nowrap}.panel code{font-size:.82em;background:var(--bg-alt);padding:0 .3em;border-radius:4px}
.viewer .hint b{font-weight:700}
.in-head{display:flex;align-items:baseline;gap:10px;margin:0 0 .6rem}.in-head b{font-size:.72rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--primary)}
.in-head span{font-size:.85rem;color:var(--text-muted)}
.case-input{background:var(--bg-alt);border:1px solid var(--line-soft);border-radius:var(--radius-sm);padding:1rem 1.2rem 1.1rem}
.in-prompt{margin:0;font-family:"DM Sans",Inter,sans-serif;font-size:1rem;line-height:1.7;color:var(--text-h);border-left:3px solid var(--primary);padding:.1rem 0 .1rem .9rem}
.in-i2s{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:1.2rem;align-items:start}
.in-refs{display:grid;gap:.8rem}.in-refs figure{margin:0}.in-refs img{width:100%;display:block;border-radius:8px;border:1px solid var(--line)}
.in-refs figcaption{font-size:.8rem;color:var(--text-muted);margin-top:.35rem}
.in-task h5{margin:0 0 .4rem;font-size:.8rem;font-weight:700;color:var(--text-h)}.in-task h5+.in-prompt{margin-bottom:1rem}
.in-task p{font-size:.9rem;color:var(--text-secondary);margin:0 0 .8rem}
.in-open{border:1px solid var(--primary);background:#fff;color:var(--primary-dark);border-radius:8px;padding:.45rem .9rem;font:600 .85rem Inter,sans-serif;cursor:pointer}
.in-open:hover{background:var(--primary);color:#fff}
@media(max-width:900px){.in-i2s{grid-template-columns:1fr}}
</style><script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"}}</script></head><body>
<nav class="navbar"><a class="navbar-brand" href="index.html"><svg viewBox="0 0 32 32" aria-hidden="true"><defs><linearGradient id="gi" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#92400e"/><stop offset="1" stop-color="#d97706"/></linearGradient></defs><rect x="1" y="1" width="30" height="30" rx="8" fill="url(#gi)"/><path d="M16 6.5 25 11.5v9L16 25.5 7 20.5v-9z" fill="none" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><path d="M7 11.5 16 16.5 25 11.5M16 16.5v9" fill="none" stroke="#fff" stroke-width="2" stroke-linejoin="round"/></svg>Code4Scene</a><ul class="navbar-links"><li class=""><a href="index.html#leaderboard">Leaderboard</a></li><li class="hide-sm"><a href="index.html#takeaways">Takeaways</a></li><li class=""><a href="cases.html" class=on>Cases in 3D</a></li><li class="hide-sm"><a href="https://github.com/SimWorld-AI/Code4Scene" target="_blank" rel="noopener"><svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg> Code</a></li><li><a class="btn-nav-primary" href="https://simworld.org/" target="_blank" rel="noopener"><img src="img/simworld-logo.png" alt="">SimWorld Home</a></li></ul></nav>
<section class="alt" style="padding-top:calc(var(--nav-h) + 3rem);padding-bottom:3rem"><div class="container">
<div class="section-header" style="margin-bottom:1.6rem"><span class="section-label">Cases</span><h2 class="section-title">Every scene in 3D, with its scores</h2>
<p class="section-desc">Each agent's result is the engine-native scene it saved, exported from Unreal Engine so you can turn it in the browser. Beside it, how the
evaluator scored that scene. 6 cases, 41 scenes in 3D. Pick a case, then an agent.</p></div>
<div class="casebar" id="casebar"></div>
<div class="card" style="padding:1.4rem 1.5rem"><div class="case-head"><h3 id="ctitle"></h3><p><b id="ctrack"></b> · <span id="cnote"></span></p></div>
<div class="case-input" id="cinput"></div>
<div class="in-head" style="margin-top:1.4rem"><b>Output</b><span>each agent's saved scene, and how the evaluator scored it</span></div>
<div class="casegrid"><div><div class="viewer" id="viewer"></div><div class="vtabs" id="vtabs"></div><div class="agents" id="agents"></div></div><div id="side"></div></div></div>
<div class="card" style="margin-top:1.5rem;padding:1.4rem 1.5rem"><div class="sub-title">Every agent on this case</div>
<div class="sub-desc">The evaluator's scores for all 14 agents on the selected case. Click a row to open that agent's scene.</div><div class="board-wrap" id="ctable"></div></div>
</div></section>
<footer><p>Code4Scene &nbsp;·&nbsp; UC San Diego &amp; UC Berkeley &nbsp;·&nbsp; <a href="https://github.com/SimWorld-AI/Code4Scene" target="_blank" rel="noopener">GitHub</a> &nbsp;·&nbsp;
<a href="https://simworld.org/" target="_blank" rel="noopener">SimWorld Home</a></p>
<p style="margin-top:.5rem;font-size:.75rem;color:rgba(255,255,255,.25)">Part of the SimWorld project.</p></footer>
<script src="assets/paper.js?v=5427505a"></script><script src="data/site.js?v=0b09af46"></script><script type="module">import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { MeshoptDecoder } from "three/addons/libs/meshopt_decoder.module.js";
import { Sky } from "three/addons/objects/Sky.js";
const S = window.SITE, $ = id => document.getElementById(id), f3 = v => (v == null ? "–" : v.toFixed(3));
const name = k => S.agents[k].name, short = k => name(k).replace(/ \(.*\)/, ""), logo = k => `<span class="logo">${S.logos[S.agents[k].logo] || ""}</span>`;
let cur = null, agent = null, view = null, three = null;
// three.js: one renderer, moved between loads; each GLB's scene is cached. want3d is the url the viewer should show now.
const glbCache = {}, pending = {};
let want3d = null, framedFor = null;
function viewer3d(url, label) {
const el = $("viewer"); want3d = url; el.innerHTML = '<div class="load">Loading the 3D scene…</div>';
if (!three) {
const r = new THREE.WebGLRenderer({ antialias: true }); r.setPixelRatio(Math.min(2, devicePixelRatio)); r.outputColorSpace = THREE.SRGBColorSpace;
r.toneMapping = THREE.ACESFilmicToneMapping; r.toneMappingExposure = 1.0;
r.shadowMap.enabled = true; r.shadowMap.type = THREE.PCFSoftShadowMap;
const cam = new THREE.PerspectiveCamera(45, 16 / 9, .05, 200000), ctl = new OrbitControls(cam, r.domElement);
ctl.enableDamping = true; ctl.autoRotate = true; ctl.autoRotateSpeed = .6;
ctl.addEventListener("start", () => { ctl.autoRotate = false; });
// daylight for every scene: a physical sky, prefiltered once into an environment map (image-based light and background)
const SUN = new THREE.Vector3().setFromSphericalCoords(1, THREE.MathUtils.degToRad(90 - 38), THREE.MathUtils.degToRad(215));
const sky = new Sky(); sky.scale.setScalar(1000);
Object.assign(sky.material.uniforms, {}); sky.material.uniforms.turbidity.value = 4; sky.material.uniforms.rayleigh.value = 1.4;
sky.material.uniforms.mieCoefficient.value = .004; sky.material.uniforms.mieDirectionalG.value = .8; sky.material.uniforms.sunPosition.value.copy(SUN);
const skyScene = new THREE.Scene(); skyScene.add(sky);
const env = new THREE.PMREMGenerator(r).fromScene(skyScene, 0, .1, 2000).texture;
three = { r, cam, ctl, scene: null, env, SUN };
const loop = () => { requestAnimationFrame(loop); if (three.scene && r.domElement.isConnected) { ctl.update(); r.render(three.scene, cam); } }; loop();
new ResizeObserver(() => { const w = el.clientWidth, h = el.clientHeight; if (!w) return; r.setSize(w, h, false); cam.aspect = w / h; cam.updateProjectionMatrix(); }).observe(el);
}
const show = sc => {
if (want3d !== url || !(view || "").startsWith("3d")) return;
el.innerHTML = ""; el.appendChild(three.r.domElement); three.r.domElement.style.width = "100%"; three.r.domElement.style.height = "100%";
three.r.setSize(el.clientWidth, el.clientHeight, false); three.cam.aspect = el.clientWidth / el.clientHeight;
three.scene = sc.scene;
// Image-to-Scene: the repair, the ground truth and the corrupted input share one map, so the camera stays put between them
// and the three can be compared from the same place; it starts on the target actors.
const key = cur.track === "i2s" ? cur.id : url;
if (framedFor !== key && cur.track === "i2s" && cur.ref_cam) {
// open where the reference image was taken: its camera, its horizontal field of view
const rc = cur.ref_cam, asp = el.clientWidth / el.clientHeight;
three.cam.fov = 2 * Math.atan(Math.tan(rc.hfov * Math.PI / 360) / asp) * 180 / Math.PI;
three.cam.near = .05; three.cam.far = sc.box.getSize(new THREE.Vector3()).length() * 12 + 1000;
three.cam.position.set(...rc.pos); three.ctl.target.set(...rc.target); three.ctl.autoRotate = false; framedFor = key;
} else if (framedFor !== key) {
three.cam.fov = 45;
const f = sc.focus || sc.box, c = f.getCenter(new THREE.Vector3());
const s = sc.focus ? Math.max(9, f.getSize(new THREE.Vector3()).length() * 3.2) : sc.box.getSize(new THREE.Vector3()).length() * 1.2;
three.cam.near = s / 800; three.cam.far = Math.max(s, sc.box.getSize(new THREE.Vector3()).length()) * 12;
three.cam.position.copy(c).add(new THREE.Vector3(-.45, .62, .64).normalize().multiplyScalar(s));
three.ctl.target.copy(c); three.ctl.autoRotate = !sc.focus; framedFor = key;
}
three.cam.updateProjectionMatrix(); three.ctl.update();
const legend = sc.marks ? ' · <b style="color:#5ee08a">green</b> target · <b style="color:#ff6b5e">red</b> duplicate to delete' : "";
el.insertAdjacentHTML("beforeend", `<div class="hint">3D · ${label}${legend} · drag, scroll, right-drag</div>`);
};
if (glbCache[url]) return show(glbCache[url]);
if (pending[url]) return;
pending[url] = true;
const targets = cur.track === "i2s" ? (cur.targets || []) : [], dups = cur.track === "i2s" ? (cur.duplicates || []) : [];
const onLoad = g => {
const sc = new THREE.Scene(); sc.background = three.env; sc.environment = three.env; sc.environmentIntensity = .8;
sc.add(g.scene);
// UE's specular (KHR_materials_specular up to ~2) makes sky-lit surfaces read as white; keep it at most glTF's default
g.scene.traverse(o => { if (!o.isMesh) return; o.castShadow = o.receiveShadow = true;
for (const m of [].concat(o.material)) if (m && m.specularIntensity !== undefined) m.specularIntensity = Math.min(m.specularIntensity, .5); });
// the box of the scene's bulk, not of a stray far-away actor (sky spheres, fog volumes)
const box = new THREE.Box3(), pts = [];
g.scene.traverse(o => { if (o.isMesh) { const b = new THREE.Box3().setFromObject(o); if (!b.isEmpty()) pts.push(b); } });
const q = (arr, t) => arr.slice().sort((a, b) => a - b)[Math.floor(t * (arr.length - 1))];
if (pts.length > 8) {
const cx = pts.map(b => (b.min.x + b.max.x) / 2), cy = pts.map(b => (b.min.y + b.max.y) / 2), cz = pts.map(b => (b.min.z + b.max.z) / 2);
box.set(new THREE.Vector3(q(cx, .03), q(cy, .03), q(cz, .03)), new THREE.Vector3(q(cx, .97), q(cy, .97), q(cz, .97)));
} else box.setFromObject(g.scene);
// the actors the task is about, boxed: the target (green) and the duplicate the corruption added (red, should be gone)
let focus = null, marks = 0;
if (targets.length) {
g.scene.updateMatrixWorld(true);
g.scene.traverse(o => {
const dup = dups.includes(o.name);
if (!dup && !targets.includes(o.name)) return;
const b = new THREE.Box3().setFromObject(o); if (b.isEmpty()) return;
// drawn over everything, so a box behind a tree or a fence still shows where the actor is
const h = new THREE.Box3Helper(b.clone().expandByScalar(.08), dup ? 0xff6b5e : 0x5ee08a);
h.material.depthTest = false; h.material.transparent = true; h.renderOrder = 999; sc.add(h); marks++;
if (!dup) focus = focus ? focus.union(b) : b.clone();
else if (!focus) focus = b.clone();
});
}
const ctr = box.getCenter(new THREE.Vector3()), rad = Math.max(8, box.getSize(new THREE.Vector3()).length() / 2);
const sun = new THREE.DirectionalLight(0xfff1dc, 3.2); sun.position.copy(ctr).addScaledVector(three.SUN, rad * 2); sun.target.position.copy(ctr);
sun.castShadow = true; sun.shadow.mapSize.set(4096, 4096); sun.shadow.bias = -.0004; sun.shadow.normalBias = .02;
Object.assign(sun.shadow.camera, { left: -rad, right: rad, top: rad, bottom: -rad, near: rad * .5, far: rad * 4 }); sun.shadow.camera.updateProjectionMatrix();
sc.add(sun, sun.target);
glbCache[url] = { scene: sc, box, focus, marks }; delete pending[url]; show(glbCache[url]);
};
const fail = () => { delete pending[url]; if (want3d === url) el.innerHTML = '<div class="load">Could not load the 3D scene.</div>'; };
const progress = f => { const l = el.querySelector(".load"); if (l && want3d === url) l.textContent = `Loading the 3D scene… ${Math.round(f * 100)}%`; };
fetchPacked(url, progress).then(buf => new GLTFLoader().setMeshoptDecoder(MeshoptDecoder).parse(buf, "", onLoad, fail)).catch(fail);
}
// The served scenes are packed (pack_models.py): the GLB XORed against this keystream, so a saved file is not a usable model.
const KEY = Uint8Array.from("5c2f9e71d3a84b06e1f7c29a38d05b64a7193ce8f2046db95e81c73a2df6904b".match(/../g), h => parseInt(h, 16));
function unpack(a) { for (let i = 0; i < a.length; i++) a[i] ^= KEY[i & 31] ^ (((i >>> 5) * 131) & 255); return a.buffer; }
async function fetchPacked(url, progress) {
const r = await fetch(url); if (!r.ok) throw new Error(r.status);
const total = +r.headers.get("content-length") || 0, reader = r.body.getReader(), parts = []; let got = 0;
for (;;) { const { done, value } = await reader.read(); if (done) break; parts.push(value); got += value.length; if (total) progress(got / total); }
const a = new Uint8Array(got); let o = 0; for (const p of parts) { a.set(p, o); o += p.length; }
return unpack(a);
}
function showImage(src, hint) { $("viewer").innerHTML = src ? `<img src="${src}" alt=""><div class="hint">${hint}</div>` : '<div class="load">This run saved no render.</div>'; }
function drawViewer() {
const a = agent, tabs = [];
want3d = null;
if (a.glb) tabs.push(["3d", cur.track === "t2s" ? "3D scene" : "3D · repair"]);
if (cur.track === "i2s") { if (cur.gt_glb) tabs.push(["3dgt", "3D · ground truth"]); if (cur.input_glb) tabs.push(["3din", "3D · corrupted input"]); }
if (cur.track === "t2s") {
a.views.forEach((v, i) => tabs.push(["v" + i, `Judge view ${i + 1}`]));
if (!a.views.length && a.img) tabs.push(["img", "Judge view"]);
if (a.judge) tabs.push(["judge", "Judge's evidence"]);
} else {
tabs.push(["res", "Repair, target view"]); if (cur.gt) tabs.push(["gt", "Ground truth"]); if (cur.input) tabs.push(["in", "Corrupted input"]);
}
if (!tabs.find(t => t[0] === view)) view = tabs.length ? tabs[0][0] : null;
$("vtabs").innerHTML = tabs.map(([k, l]) => `<button data-v="${k}" class="${k === view ? "on" : ""}">${l}</button>`).join("");
$("vtabs").querySelectorAll("button").forEach(b => b.onclick = () => { view = b.dataset.v; drawViewer(); });
if (view === "3d") return viewer3d(a.glb, cur.track === "t2s" ? `${short(a.key)}'s saved scene` : `${short(a.key)}'s repair, from the reference camera`);
if (view === "3dgt") return viewer3d(cur.gt_glb, "ground truth");
if (view === "3din") return viewer3d(cur.input_glb, "corrupted input");
if (!view) return showImage(null);
if (/^v\d/.test(view)) return showImage(a.views[+view.slice(1)], "One of the four overview views the judge scores (Overview Alignment)");
if (view === "img") return showImage(a.img, "The judge's overview view");
if (view === "judge") return showImage(a.judge, "The judge's own evidence frame for one requirement");
if (view === "res") return showImage(a.img, "The agent's repaired scene, rendered from the target view");
if (view === "gt") return showImage(cur.gt, "The withheld ground truth, same view");
if (view === "in") return showImage(cur.input, "The corrupted scene the agent starts from");
if (view.startsWith("ref")) return showImage(cur.refs[+view.slice(3)], "A reference image given to the agent");
}
const bar = (label, v, w) => `<div class="sc-row"><span>${label}${w ? ` <span class="w">× ${w}</span>` : ""}</span><div class="tr"><div class="fi" style="width:${((v || 0) * 100).toFixed(1)}%"></div></div><span class="v">${f3(v)}</span></div>`;
function drawSide() {
const a = agent, rank = cur.agents.indexOf(a) + 1;
let h = `<div class="panel"><h4>${logo(a.key)} ${name(a.key)}</h4><p class="rk">Rank ${rank} of ${cur.agents.length} on this case</p>
<div class="sc-big">${f3(a.s)}</div><p class="rk" style="margin-top:-4px">case score</p>`;
if (cur.track === "t2s") h += bar("Detailed Alignment", a.detailed) + bar("Overview Alignment", a.overview) + bar("Physical Safety", a.phys)
+ `<div class="formula">0.2 × ${f3(a.detailed)} + 0.6 × ${f3(a.overview)} + 0.2 × ${f3(a.phys)} = ${f3(.2 * a.detailed + .6 * a.overview + .2 * a.phys)}</div>`;
else h += bar("Repair F1", a.f1) + bar("Physical Safety", a.phys)
+ `<div class="formula">Target actors: ${a.tp} recovered · ${a.fp} wrong or extra · ${a.fn} missed.<br>0.8 × ${f3(a.f1)} + 0.2 × ${f3(a.phys)} = ${f3(.8 * a.f1 + .2 * a.phys)}</div>`;
h += `</div>`;
if (a.findings) h += `<div class="panel"><h4>What the evaluator found</h4>${a.findings.map(([k, t]) => `<div class="find ${k}"><span class="m">${k === "match" ? "✓" : k === "mismatch" ? "✗" : "!"}</span><span>${t}</span></div>`).join("")}</div>`;
if (cur.track === "i2s" && cur.corruption && cur.corruption.length) {
const m = v => Math.hypot(v[0], v[1]) / 100;
const say = o => o.type === "duplication" ? `added a copy of <code>${o.label}</code>, ${m(o.delta_cm).toFixed(1)} m away (it should be deleted)`
: o.type === "translation" ? `moved <code>${o.label}</code> ${m(o.delta_cm).toFixed(1)} m`
: o.type === "rotation" ? `turned <code>${o.label}</code> ${Math.abs(o.yaw_deg)}°`
: o.type === "scaling" ? `rescaled <code>${o.label}</code> (${[].concat(o.factor).map(x => "×" + x).join(", ")})` : `${o.type} on <code>${o.label}</code>`;
h += `<div class="panel"><h4>What the corruption did</h4>${cur.corruption.map(o => `<div class="find mismatch"><span class="m">·</span><span>${say(o)}</span></div>`).join("")}
<p class="rk" style="margin-top:8px">The agent sees only the corrupted scene and the reference image. Repair F1 counts the target actors it restores to the ground truth; changes to anything else count against it.</p></div>`;
}
$("side").innerHTML = h;
}
function drawAgents() {
$("agents").innerHTML = cur.agents.map((a, i) => `<button class="ag ${a === agent ? "on" : ""}" data-i="${i}" title="${name(a.key)}"><div class="th" style="background-image:url('${a.img || ""}')">${a.glb ? "<i>3D</i>" : ""}</div>
<div class="cap"><b>${f3(a.s)}</b><br>${short(a.key)}</div></button>`).join("");
$("agents").querySelectorAll(".ag").forEach(el => el.onclick = () => select(cur.agents[+el.dataset.i]));
}
// every agent on this case, as the evaluator scored it
function drawTable() {
const t2s = cur.track === "t2s";
const cols = t2s ? [["s", "Case score"], ["detailed", "Detailed"], ["overview", "Overview"], ["phys", "Physical"]] : [["s", "Case score"], ["f1", "Repair F1"], ["phys", "Physical"]];
const head = `<tr><th>#</th><th>Agent</th>${cols.map(([, l]) => `<th>${l}</th>`).join("")}${t2s ? "" : "<th>Recovered · wrong · missed</th>"}<th>3D</th></tr>`;
const body = cur.agents.map((a, i) => `<tr class="${a === agent ? "on" : ""}" data-i="${i}"><td class="n">${i + 1}</td><td class="nm">${logo(a.key)} ${name(a.key)}</td>`
+ cols.map(([k], j) => j === 0 ? `<td><div class="cellbar"><div style="width:${(a[k] * 100).toFixed(1)}%"></div><b>${f3(a[k])}</b></div></td>` : `<td>${f3(a[k])}</td>`).join("")
+ (t2s ? "" : `<td>${a.tp} · ${a.fp} · ${a.fn}</td>`) + `<td>${a.glb ? "●" : ""}</td></tr>`).join("");
$("ctable").innerHTML = `<table class="cs">${head}${body}</table>`;
$("ctable").querySelectorAll("tr[data-i]").forEach(el => el.onclick = () => { select(cur.agents[+el.dataset.i]); $("viewer").scrollIntoView({ behavior: "smooth", block: "center" }); });
}
function select(a) { agent = a; if (!a.glb && view === "3d") view = null; drawAgents(); drawViewer(); drawSide(); drawTable(); }
function pick(id) {
cur = S.cases.find(c => c.id === id); view = null;
$("ctitle").textContent = cur.title; $("ctrack").textContent = cur.track === "t2s" ? "Text-to-Scene · construction" : "Image-to-Scene · editing";
$("cnote").textContent = cur.note;
document.querySelectorAll("#casebar button").forEach(b => b.classList.toggle("on", b.dataset.id === id));
history.replaceState(null, "", `#${id}`);
drawInput();
select(cur.agents.find(a => a.glb) || cur.agents[0]);
}
// what every agent was given, shown as given: the prompt, or the reference image(s) and the instruction
function drawInput() {
if (cur.track === "t2s") {
$("cinput").innerHTML = `<div class="in-head"><b>Input</b><span>the same prompt for all 14 agents, with an empty level and the content pack's asset catalog</span></div>
<blockquote class="in-prompt">${cur.prompt}</blockquote>`;
return;
}
const refs = cur.refs.map((r, i) => `<figure><img src="${r}" alt="Reference image ${i + 1}"><figcaption>Reference image${cur.refs.length > 1 ? " " + (i + 1) : ""}, as given to the agent</figcaption></figure>`).join("");
$("cinput").innerHTML = `<div class="in-head"><b>Input</b><span>the same for all 14 agents</span></div>
<div class="in-i2s"><div class="in-refs">${refs}</div>
<div class="in-task"><h5>Instruction</h5><blockquote class="in-prompt">${cur.prompt}</blockquote>
<h5>Starting scene</h5><p>A copy of the human-assembled scene with a few actors corrupted. The agent sees it only through the engine.</p>
${cur.input_glb ? '<button class="in-open" id="open-input">Open the corrupted scene in 3D →</button>' : ""}</div></div>`;
const b = $("open-input");
if (b) b.onclick = () => { view = "3din"; drawViewer(); $("viewer").scrollIntoView({ behavior: "smooth", block: "center" }); };
}
// one row per setting: its name, then its cases; each case says how many of its scenes open in 3D
const btn = c => { const n = c.agents.filter(a => a.glb).length + (c.gt_glb ? 1 : 0) + (c.input_glb ? 1 : 0);
return `<button data-id="${c.id}"><span>${c.title}</span><small>${n} scenes</small></button>`; };
const row = (track, name, kind) => `<div class="cb-row"><div class="grp"><b>${name}</b><span>${kind}</span></div><div class="cb-btns">`
+ S.cases.filter(c => c.track === track).map(btn).join("") + `</div></div>`;
$("casebar").innerHTML = row("t2s", "Text-to-Scene", "construction") + row("i2s", "Image-to-Scene", "editing");
$("casebar").querySelectorAll("button").forEach(b => b.onclick = () => pick(b.dataset.id));
const h = location.hash.slice(1);
pick(S.cases.find(c => c.id === h) ? h : S.cases[0].id);
addEventListener("hashchange", () => { const id = location.hash.slice(1); if (S.cases.find(c => c.id === id) && (!cur || cur.id !== id)) pick(id); });
</script>
</body></html>