Spaces:
Running
Running
Download cases.html from SimWorld-AI/Code4Scene: direct link, hf CLI and curl.
- Browser
- Download file 32.1 kB
-
https://huggingface.co/spaces/SimWorld-AI/Code4Scene/resolve/main/cases.html
- Command line
-
hf download hf://spaces/SimWorld-AI/Code4Scene/cases.html
-
curl -L -o cases.html https://huggingface.co/spaces/SimWorld-AI/Code4Scene/resolve/main/cases.html
32.1 kB
| <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's saved scene in 3D and the evaluator'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);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 · UC San Diego & UC Berkeley · <a href="https://github.com/SimWorld-AI/Code4Scene" target="_blank" rel="noopener">GitHub</a> · | |
| <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> |