1// 1) IMPORTS (ensure your bundler or <script type="module"> can find these) 2import * as THREE from 'three'; 3import { PCDLoader } from 'PCDLoader'; 4import { OrbitControls } from 'OrbitControls'; 5// PLY loader from Three.js examples for mesh .ply files (used in comparison grid) 6import { PLYLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders/PLYLoader.js'; 7 8// 2) SCENE MAP (scene_1/2/3 -> existing PCD object names; for now same PCD used in all 6 cells) 9const sceneToPcd = { 10 scene_1: 'partnet_78', 11 scene_2: 'partnet_652', 12 scene_3: 'partnet_680' 13}; 14const sampleMap = { 15 partnet_78: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19'], 16 partnet_652: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19'], 17 partnet_680: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19'] 18}; 19 20// OUTSIDEâat module scope, create and reuse these: 21const _trajSphereGeo = new THREE.SphereGeometry(0.008, 6, 4); 22const _trajSphereMat = new THREE.MeshBasicMaterial({ 23 transparent: true, 24 opacity: 0.35, 25 vertexColors: true, // allows perâinstance color 26 // Because it's MeshBasicMaterial, it does NOT cast or receive shadows 27}); 28 29const viewerParams = { 30 partnet_78: { groundHeight: -0.8, cameraY: 1.5 }, 31 partnet_652: { groundHeight: -0.85, cameraY: 1.5 }, 32 partnet_680: { groundHeight: -0.45, cameraY: 1.0 }, 33 scene_1: { groundHeight: -0.8, cameraY: 1.5 }, 34 scene_2: { groundHeight: -0.85, cameraY: 1.5 }, 35 scene_3: { groundHeight: -0.45, cameraY: 1.0 } 36}; 37 38const totalFrames = 20; // steps per sample 39const frameInterval = 40; // ms per frame 40const pauseDuration = 3000; // ms to pause at last frame 41let slowMode = true; // toggle âslowâ vs ânormalâ playback 42 43// 3) HSV â RGB HELPER (unchanged) 44function hsvToRgb(h, s, v) { 45 let r, g, b; 46 const i = Math.floor(h * 6); 47 const f = h * 6 - i; 48 const p = v * (1 - s); 49 const q = v * (1 - f * s); 50 const t = v * (1 - (1 - f) * s); 51 switch (i % 6) { 52 case 0: r = v; g = t; b = p; break; 53 case 1: r = q; g = v; b = p; break; 54 case 2: r = p; g = v; b = t; break; 55 case 3: r = p; g = q; b = v; break; 56 case 4: r = t; g = p; b = v; break; 57 case 5: r = v; g = p; b = q; break; 58 } 59 return [r, g, b]; 60} 61 62// 4) GLOBAL STATE 63let isPaused = false; 64let autoResample = true; 65let currentFrameIdx = 0; 66let playTimeoutId = null; 67let currentSession = 0; // âsession stampâ to cancel stale loads/timeouts 68let isSyncing = false; // guard for OrbitControl sync 69let inputState = null; 70let sampledStates = []; 71let allStates = []; 72 73const globalLoader = new PCDLoader(); 74const globalPlyLoader = new PLYLoader(); 75 76// --- TRAJ GLOBALS --- 77let showTrajectories = false; // tracks whether trajectories should be visible 78// (Each viewer state will keep its own `trajectorySpheres` array.) 79 80// 5) UTILITY: PICK TWO DISTINCT RANDOM SAMPLES (unchanged) 81function pickTwoRandomSamples(objName) { 82 const arr = sampleMap[objName]; 83 if (!arr || arr.length < 2) { 84 console.error(`Need â¥2 samples for "${objName}".`); 85 return arr.slice(0, 2); 86 } 87 const copy = arr.slice(); 88 for (let i = copy.length - 1; i > 0; i--) { 89 const j = Math.floor(Math.random() * (i + 1)); 90 [copy[i], copy[j]] = [copy[j], copy[i]]; 91 } 92 return [copy[0], copy[1]]; 93} 94 95// 6) SYNC HELPERS FOR ORBITCONTROLS (unchanged) 96function syncAnglesFrom(srcControls) { 97 if (isSyncing) return; 98 isSyncing = true; 99 try { 100 const cam = srcControls.object; 101 const target = srcControls.target.clone(); 102 const offset = new THREE.Vector3().copy(cam.position).sub(target); 103 const spherical = new THREE.Spherical().setFromVector3(offset); 104 105 allStates.forEach((st) => { 106 if (st.controls === srcControls) return; 107 const newPos = new THREE.Vector3() 108 .setFromSphericalCoords(spherical.radius, spherical.phi, spherical.theta) 109 .add(st.controls.target); 110 st.camera.position.copy(newPos); 111 st.camera.lookAt(st.controls.target); 112 st.controls.update(); 113 }); 114 } finally { 115 isSyncing = false;
116 } 117} 118 119// 7) INITIALIZE A STATIC INPUT VIEWER (colored by label) 120function initInputViewer(container, config) { 121 let width = container.clientWidth; 122 let height = container.clientHeight; 123 if (!width || !height) { 124 width = Math.max(width || 200, 200); 125 height = Math.max(height || 200, 200); 126 } 127 128 const scene = new THREE.Scene(); 129 scene.background = new THREE.Color(0xFCFCFC); 130 131 const aspect = height > 0 ? width / height : 1; 132 const camera = new THREE.PerspectiveCamera(25, aspect, 0.1, 1000); 133 // Use Z-up convention so auto-rotation orbits around a horizontal axis 134 camera.up.set(0, 0, 1); 135 camera.position.set(0, 1.4, 5); 136 camera.lookAt(0, 0, 0); 137 138 const renderer = new THREE.WebGLRenderer({ antialias: true }); 139 renderer.setSize(width, height); 140 renderer.shadowMap.enabled = true; 141 renderer.shadowMap.type = THREE.PCFSoftShadowMap; 142 container.appendChild(renderer.domElement); 143 144 // Lights 145 const ambient = new THREE.AmbientLight(0xffffff, 1.8); 146 scene.add(ambient); 147 const dirLight = new THREE.DirectionalLight(0xffffff, 1); 148 dirLight.position.set(5, 12, 5); 149 dirLight.castShadow = true; 150 dirLight.shadow.mapSize.width = 1024; 151 dirLight.shadow.mapSize.height = 1024; 152 dirLight.shadow.camera.near = 1; 153 dirLight.shadow.camera.far = 25; 154 dirLight.shadow.camera.left = -10; 155 dirLight.shadow.camera.right = 10; 156 dirLight.shadow.camera.top = 10; 157 dirLight.shadow.camera.bottom = -10; 158 scene.add(dirLight); 159 160 // Handle window resize 161 window.addEventListener('resize', () => { 162 const w = container.clientWidth; 163 const h = container.clientHeight; 164 renderer.setSize(w, h); 165 camera.aspect = w / h; 166 camera.updateProjectionMatrix(); 167 }); 168 169 // âLoadingâ¦â overlay 170 const loading = document.createElement('div'); 171 loading.classList.add('loading'); 172 loading.innerText = 'Loadingâ¦'; 173 container.appendChild(loading); 174 175 // OrbitControls (autoârotate + dragâpause + sync) 176 const controls = new OrbitControls(camera, renderer.domElement); 177 controls.target.set(0, 0, 0); 178 controls.enablePan = false; 179 // Allow zooming with mouse wheel / trackpad 180 controls.enableZoom = true; 181 controls.autoRotate = true; 182 // Even slower, more gentle rotation 183 controls.autoRotateSpeed = 0.15; 184 controls.update(); 185 186 controls.userIsInteracting = false; 187 controls.domElement.addEventListener('mousedown', () => { 188 controls.userIsInteracting = true; 189 }); 190 document.addEventListener('mouseup', () => { 191 controls.userIsInteracting = false; 192 }); 193 194 controls.addEventListener('start', () => { 195 allStates.forEach((st) => { 196 st.controls.autoRotate = false; 197 }); 198 document.getElementById('btn-rotate').disabled = true; 199 }); 200 controls.addEventListener('end', () => { 201 const rotateBtn = document.getElementById('btn-rotate'); 202 const isAutoRotate = rotateBtn.innerHTML.includes(': On'); 203 document.getElementById('btn-rotate').disabled = false; 204 if (!isAutoRotate) return; 205 allStates.forEach((st) => { 206 st.controls.autoRotate = true; 207 }); 208 syncAnglesFrom(controls); 209 }); 210 // Sync all viewers whenever any control changes (rotation or zoom) 211 controls.addEventListener('change', () => { 212 syncAnglesFrom(controls); 213 }); 214 215 // LOAD 3D mesh PLY files and display them overlapped: 216 // - RGB geometry mesh (underlay) 217 // - Instance mesh (colored overlay), using the same coordinate frame 218 // The specific files for this viewer are provided via config. 219 const rgbUrl = (config && typeof config === 'object' && config.rgbUrl) 220 ? config.rgbUrl 221 : 'pcd/scene0170_00-scene0170_01/scene0170_00_rgb.ply'; 222 const instUrl = (config && typeof config === 'object' && config.instUrl) 223 ? config.instUrl 224 : 'pcd/scene0170_00-scene0170_01/scene0170_00_gt_instances_filtered.ply'; 225 226 globalPlyLoader.load( 227 rgbUrl, 228 (geometry) => { 229 geometry.computeVertexNormals(); 230 geometry.computeBoundingSphere(); 231 232 const hasColor = geometry.hasAttribute('color'); 233 const geomMat = new THREE.MeshStandardMaterial({ 234 // Very bright base so RGB vertex colors appear vivid 235 color: 0xffffff, 236 metalness: 0.0, 237 roughness: 0.4, 238 flatShading: false, 239 vertexColors: hasColor, 240 // Add a small emissive term to boost brightness without washing out colors 241 emissive: new THREE.Color(0.15, 0.15, 0.15), 242 emissiveIntensity: 1.2 243 }); 244 const geomMesh = new THREE.Mesh(geometry, geomMat); 245 geomMesh.castShadow = true; 246 geomMesh.receiveShadow = true; 247 scene.add(geomMesh); 248 249 // Fit camera to geometry mesh 250 const bs = geometry.boundingSphere; 251 const center = bs.center; 252 const radius = bs.radius || 1.0; 253 camera.position.set(center.x + 2.5 * radius, center.y + 1.5 * radius, center.z + 2.5 * radius); 254 controls.target.copy(center); 255 controls.update(); 256 257 loading.style.display = 'none'; 258 259 // After geometry is in place, load instance mesh and overlay it. 260 globalPlyLoader.load( 261 instUrl, 262 (instGeom) => { 263 instGeom.computeVertexNormals(); 264 const instHasColor = instGeom.hasAttribute('color'); 265 const instMat = new THREE.MeshStandardMaterial({ 266 color: 0xffffff, 267 metalness: 0.0, 268 roughness: 0.7, 269 flatShading: false,
270 vertexColors: instHasColor, 271 transparent: true, 272 // Slightly more transparent so underlying geometry is visible 273 opacity: 0.7 274 }); 275 const instMesh = new THREE.Mesh(instGeom, instMat); 276 instMesh.castShadow = false; 277 instMesh.receiveShadow = false; 278 scene.add(instMesh); 279 }, 280 undefined, 281 (err2) => { 282 console.error(`Error loading instance mesh PLY ${instUrl}:`, err2); 283 } 284 ); 285 }, 286 undefined, 287 (err) => { 288 console.error(`Error loading geometry mesh PLY ${rgbUrl}:`, err); 289 loading.innerText = 'Error loading mesh'; 290 } 291 ); 292 293 return { 294 container, 295 scene, 296 camera, 297 renderer, 298 controls, 299 mesh: null, 300 isSampled: false 301 }; 302} 303 304// 8) INITIALIZE A âSAMPLEDâ VIEWER (20âframe animation) 305function initSampledViewer(container, objName, initialSampleId) { 306 const width = container.clientWidth; 307 const height = container.clientHeight; 308 309 // Scene, camera, renderer 310 const scene = new THREE.Scene(); 311 scene.background = new THREE.Color(0xFCFCFC); 312 313 const camera = new THREE.PerspectiveCamera(25, width / height, 0.1, 1000); 314 camera.position.set(0, viewerParams[objName].cameraY, 5); 315 camera.lookAt(0, 0, 0); 316 317 const renderer = new THREE.WebGLRenderer({ antialias: true }); 318 renderer.setSize(width, height); 319 renderer.shadowMap.enabled = true; 320 renderer.shadowMap.type = THREE.PCFSoftShadowMap; 321 container.appendChild(renderer.domElement); 322 323 // Lights 324 const ambient = new THREE.AmbientLight(0xffffff, 1.8); 325 scene.add(ambient); 326 const dirLight = new THREE.DirectionalLight(0xffffff, 1); 327 dirLight.position.set(5, 12, 5); 328 dirLight.castShadow = true; 329 dirLight.shadow.mapSize.width = 1024; 330 dirLight.shadow.mapSize.height = 1024; 331 dirLight.shadow.camera.near = 1; 332 dirLight.shadow.camera.far = 25; 333 dirLight.shadow.camera.left = -10; 334 dirLight.shadow.camera.right = 10; 335 dirLight.shadow.camera.top = 10; 336 dirLight.shadow.camera.bottom = -10; 337 scene.add(dirLight); 338 339 // Ground plane 340 const planeGeo = new THREE.PlaneGeometry(20, 20); 341 const planeMat = new THREE.ShadowMaterial({ opacity: 0.2 }); 342 const ground = new THREE.Mesh(planeGeo, planeMat); 343 ground.rotateX(-Math.PI / 2); 344 ground.position.y = viewerParams[objName].groundHeight; 345 ground.receiveShadow = true; 346 scene.add(ground); 347 348 // Handle window resize 349 window.addEventListener('resize', () => { 350 const w = container.clientWidth; 351 const h = container.clientHeight; 352 renderer.setSize(w, h); 353 camera.aspect = w / h; 354 camera.updateProjectionMatrix(); 355 }); 356 357 // âLoadingâ¦â overlay 358 const loading = document.createElement('div'); 359 loading.classList.add('loading'); 360 loading.innerText = 'Loadingâ¦'; 361 container.appendChild(loading); 362 363 // OrbitControls (autoârotate + dragâpause + sync) 364 const controls = new OrbitControls(camera, renderer.domElement); 365 controls.target.set(0, 0, 0); 366 controls.enablePan = false; 367 controls.enableZoom = false;
368 controls.autoRotate = true; 369 controls.autoRotateSpeed = 2; 370 controls.update(); 371 372 controls.userIsInteracting = false; 373 controls.domElement.addEventListener('mousedown', () => { 374 controls.userIsInteracting = true; 375 }); 376 document.addEventListener('mouseup', () => { 377 controls.userIsInteracting = false; 378 }); 379 380 controls.addEventListener('start', () => { 381 allStates.forEach((st) => { 382 st.controls.autoRotate = false; 383 }); 384 document.getElementById('btn-rotate').disabled = true; 385 }); 386 controls.addEventListener('end', () => { 387 allStates.forEach((st) => { 388 st.controls.autoRotate = true; 389 }); 390 document.getElementById('btn-rotate').disabled = false; 391 syncAnglesFrom(controls); 392 }); 393 controls.addEventListener('change', () => { 394 if (controls.userIsInteracting) { 395 syncAnglesFrom(controls); 396 } 397 }); 398 399 // Prepare arrays for 20 frames 400 const frameMeshes = new Array(totalFrames).fill(null); 401 const framePositions = new Array(totalFrames).fill(null); 402 let N_points = 0; // will be known once frame 0 loads 403 404 // Keep track of trajectory spheres so we can remove them later 405 const trajectorySpheres = []; // â TRAJ 406 407 // Assign this stateâs âsession stampâ 408 const mySession = currentSession; 409 410 const state = { 411 container, 412 scene, 413 camera, 414 renderer, 415 controls, 416 frameMeshes, 417 framePositions, 418 N_points, 419 currentSampleId: null, 420 loadingOverlay: loading, 421 isSampled: true, 422 session: mySession, 423 trajectorySpheres // â TRAJ 424 }; 425 426 // Helper: pick a random sample â current 427 function pickRandomSample() { 428 const arr = sampleMap[objName]; 429 if (arr.length === 1) return arr[0]; 430 let choice; 431 do { 432 choice = arr[Math.floor(Math.random() * arr.length)]; 433 } while (choice === state.currentSampleId); 434 return choice; 435 } 436 437 // Helper: build an interpolated InstancedMesh for frames (unchanged) 438 function buildInterpolatedMesh(t0, t1) { 439 const pos0 = state.framePositions[t0]; 440 const pos1 = state.framePositions[t1]; 441 if (!pos0 || !pos1) return null; 442 443 const sphereGeo = new THREE.SphereGeometry(0.01, 6, 4); 444 const mat = new THREE.MeshStandardMaterial({ 445 transparent: true, 446 metalness: 0.2, 447 roughness: 0.4, 448 depthWrite: true, 449 flatShading: false 450 }); 451 452 const mesh = new THREE.InstancedMesh(sphereGeo, mat, state.N_points); 453 mesh.castShadow = true; 454 mesh.receiveShadow = false; 455 456 const dummyMatrix = new THREE.Matrix4(); 457 const color = new THREE.Color(); 458 const srcMesh = state.frameMeshes[t0]; 459 460 for (let i = 0; i < state.N_points; i++) { 461 const x0 = pos0[3 * i]; 462 const y0 = pos0[3 * i + 1]; 463 const z0 = pos0[3 * i + 2]; 464 const x1 = pos1[3 * i]; 465 const y1 = pos1[3 * i + 1]; 466 const z1 = pos1[3 * i + 2]; 467 468 const xm = 0.5 * (x0 + x1); 469 const ym = 0.5 * (y0 + y1); 470 const zm = 0.5 * (z0 + z1); 471 dummyMatrix.makeTranslation(xm, ym, zm); 472 mesh.setMatrixAt(i, dummyMatrix); 473 474 if (srcMesh.instanceColor) { 475 const cA = srcMesh.instanceColor; 476 const r = cA.getX(i); 477 const g = cA.getY(i); 478 const b = cA.getZ(i); 479 color.setRGB(r, g, b); 480 } else { 481 color.setRGB(0.5, 0.5, 0.5); 482 } 483 mesh.setColorAt(i, color); 484 } 485 486 mesh.visible = false; 487 state.scene.add(mesh); 488 return mesh; 489 } 490 491 // Helper: load all frames for a sample (unchanged except return) 492 async function loadSample(sampleId, showLoading = false) { 493 if (state.session !== currentSession) return null; 494 495 if (showLoading) { 496 state.loadingOverlay.style.display = 'flex'; 497 } 498 499 const newMeshes = new Array(totalFrames).fill(null); 500 const newPositions = new Array(totalFrames).fill(null); 501 let sphereGeo = null; 502 503 for (let t = 0; t < totalFrames; t++) { 504 const url = `pcd/${objName}/sample_${sampleId}/step_${t}.pcd`; 505 506 if (state.session !== currentSession) { 507 for (let k = 0; k < t; k++) { 508 if (newMeshes[k]) { 509 newMeshes[k].geometry.dispose(); 510 newMeshes[k].material.dispose(); 511 } 512 } 513 return null; 514 } 515 516 await new Promise((resolve) => { 517 globalLoader.load( 518 url, 519 (points) => { 520 const geom = points.geometry; 521 const posAttr = geom.attributes.position; 522 const lblAttr = geom.attributes.label; 523 if (!posAttr) { 524 console.error(`No position attribute in ${url}`); 525 newMeshes[t] = null; 526 newPositions[t] = null; 527 resolve(); 528 return; 529 } 530 531 const positions = posAttr.array; 532 const N = positions.length / 3; 533 if (t === 0) { 534 state.N_points = N; 535 } 536 newPositions[t] = new Float32Array(positions); 537 538 if (!sphereGeo) sphereGeo = new THREE.SphereGeometry(0.01, 6, 4); 539 const mat = new THREE.MeshStandardMaterial({ 540 transparent: true, 541 metalness: 0.2, 542 roughness: 0.4, 543 depthWrite: true, 544 flatShading: false 545 }); 546 const instMesh = new THREE.InstancedMesh(sphereGeo, mat, N); 547 instMesh.castShadow = true; 548 instMesh.receiveShadow = false;
549 550 const dummyMatrix = new THREE.Matrix4(); 551 const color = new THREE.Color(); 552 553 let maxLabel = 0; 554 if (lblAttr) { 555 for (let i = 0; i < N; i++) { 556 if (lblAttr.array[i] > maxLabel) maxLabel = lblAttr.array[i]; 557 } 558 } 559 560 for (let i = 0; i < N; i++) { 561 const x = positions[3 * i]; 562 const y = positions[3 * i + 1]; 563 const z = positions[3 * i + 2]; 564 dummyMatrix.makeTranslation(x, y, z); 565 instMesh.setMatrixAt(i, dummyMatrix); 566 567 if (lblAttr) { 568 const lbl = lblAttr.array[i]; 569 const hue = maxLabel > 0 ? (lbl / maxLabel) * 0.8 : 0; 570 const hue_remap = (hue + 0.548) % 1; 571 const [r, g, b] = hsvToRgb(hue_remap, 0.62, 0.46); 572 color.setRGB(r, g, b); 573 } else { 574 color.setRGB(0.5, 0.5, 0.5); 575 } 576 instMesh.setColorAt(i, color); 577 } 578 579 instMesh.visible = false; 580 newMeshes[t] = instMesh; 581 state.scene.add(instMesh); 582 583 geom.dispose(); 584 points.material.dispose(); 585 resolve(); 586 }, 587 () => { /* ignore progress */ }, 588 (err) => { 589 console.error(`Error loading ${url}:`, err); 590 newMeshes[t] = null; 591 newPositions[t] = null; 592 resolve(); 593 } 594 ); 595 }); 596 597 if (state.session !== currentSession) { 598 for (let k = 0; k <= t; k++) { 599 if (newMeshes[k]) { 600 newMeshes[k].geometry.dispose(); 601 newMeshes[k].material.dispose(); 602 } 603 } 604 return null; 605 } 606 } 607 608 if (state.session !== currentSession) { 609 newMeshes.forEach((m) => { 610 if (m) { 611 m.geometry.dispose(); 612 m.material.dispose(); 613 } 614 }); 615 return null; 616 } 617 618 if (showLoading) { 619 state.loadingOverlay.style.display = 'none'; 620 621 // Dispose old frameâmeshes: 622 state.frameMeshes.forEach((oldMesh) => { 623 if (oldMesh) { 624 state.scene.remove(oldMesh); 625 oldMesh.geometry.dispose(); 626 oldMesh.material.dispose(); 627 } 628 }); 629 630 // Swap in the new 631 state.frameMeshes = newMeshes; 632 state.framePositions = newPositions; 633 state.currentSampleId = sampleId; 634 635 const idx = currentFrameIdxMapping(); 636 if (idx.isOriginal && state.frameMeshes[idx.frameIndex]) { 637 state.frameMeshes[idx.frameIndex].visible = true; 638 } 639 return null; 640 } else { 641 return { newMeshes, newPositions, sampleId }; 642 } 643 } 644 645 state.currentSampleId = initialSampleId; 646 state.pickRandom = pickRandomSample; 647 state.loadSample = loadSample; 648 649 // Initially load the first sample and show frame 0 650 loadSample(initialSampleId, true); 651 652 return state; 653 654 function currentFrameIdxMapping() { 655 if (!slowMode) { 656 return { isOriginal: true, frameIndex: currentFrameIdx, alpha: 0 }; 657 } else { 658 const g = currentFrameIdx; 659 if (g % 2 === 0) { 660 return { isOriginal: true, frameIndex: g / 2, alpha: 0 }; 661 } else { 662 const i0 = Math.floor(g / 2); 663 const i1 = i0 + 1; 664 return { isOriginal: false, frameIndex: i0, nextIndex: i1, alpha: 0.5 }; 665 } 666 } 667 } 668} 669 670// 9) TEARDOWN: CLEAR ALL VIEWERS (disposing Three.js objects) 671function clearAllViewers() { 672 currentSession++; // bump session so inâflight promises bail 673 clearTimeout(playTimeoutId); 674 675 allStates.forEach((st) => { 676 // Guard: skip any state object that doesnât have a scene 677 if (!st || !st.scene) return; // â UPDATED: avoid calling traverse on undefined 678 679 if (st.isSampled) { 680 // Remove any interpolated meshes (if still present) 681 // st.scene.traverse((obj) => { 682 // if (obj.name && obj.name.startsWith(`interp_${st.currentSampleId}_`)) { 683 // if (obj.geometry) obj.geometry.dispose(); 684 // if (obj.material) obj.material.dispose(); 685 // st.scene.remove(obj); 686 // } 687 // }); 688 689 // Remove each frameâs InstancedMesh 690 if (Array.isArray(st.frameMeshes)) {
691 st.frameMeshes.forEach((m) => { 692 if (m) { 693 st.scene.remove(m); 694 if (m.geometry) m.geometry.dispose(); 695 if (m.material) m.material.dispose(); 696 } 697 }); 698 } 699 700 // Remove any trajectory spheres 701 if (Array.isArray(st.trajectorySpheres)) { 702 st.trajectorySpheres.forEach((sphere) => { 703 if (sphere.geometry) sphere.geometry.dispose(); 704 if (sphere.material) sphere.material.dispose(); 705 st.scene.remove(sphere); 706 }); 707 st.trajectorySpheres.length = 0; 708 } 709 } else { 710 if (st.mesh) { 711 st.scene.remove(st.mesh); 712 if (st.mesh.geometry) st.mesh.geometry.dispose(); 713 if (st.mesh.material) st.mesh.material.dispose(); 714 } 715 } 716 717 if (st.renderer && st.renderer.domElement) { 718 st.container.removeChild(st.renderer.domElement); 719 st.renderer.dispose(); 720 } 721 }); 722 723 // Clear the grid container 724 const containerEl = document.querySelector('.viewers-container'); 725 if (containerEl) { 726 containerEl.innerHTML = ''; 727 } 728 729 inputState = null; 730 sampledStates = []; 731 allStates = []; 732} 733 734// 9b) Build 3Ã2 grid: columns = GT | Baseline | Ours, rows = tâ | tâ (6 viewers total) 735// Only this function builds the viewer layout; no "Condition/Generation" layout. 736// Each cell displays an RGB mesh underlay and an instance mesh overlay for a given scene + time step. 737 738// Scene configuration: folder path and timestep stems (t0, t1) for each scene button. 739const comparisonScenes = { 740 scene_1: { 741 pairDir: 'pcd/scene0170_00-scene0170_01', 742 timesteps: ['scene0170_00', 'scene0170_01'] 743 }, 744 scene_2: { 745 pairDir: 'pcd/scene0029_01-scene0029_00', 746 timesteps: ['scene0029_01', 'scene0029_00'] 747 }, 748 scene_3: { 749 pairDir: 'pcd/scene0269_00-scene0269_02', 750 timesteps: ['scene0269_00', 'scene0269_02'] 751 } 752}; 753 754function buildComparisonGrid(objName) { 755 clearAllViewers(); 756 document.querySelectorAll('.tab-button').forEach((btn) => { 757 btn.classList.toggle('active', btn.dataset.obj === objName); 758 }); 759 currentSession++; 760 761 const vc = document.querySelector('.viewers-container'); 762 if (!vc) return; 763 764 const grid = document.createElement('div'); 765 grid.className = 'comparison-grid-3x2'; 766 grid.style.display = 'grid'; 767 grid.style.gridTemplateColumns = '1fr 1fr 1fr'; 768 grid.style.gridTemplateRows = 'auto 1fr 1fr'; 769 grid.style.gap = '8px'; 770 grid.style.width = '100%'; 771 grid.style.height = '520px'; 772 773 // We use a single fixed baseline: Mask3D with semantic matching 774 const baselineName = 'mask3d_sem'; 775 const colHeaders = ['GT', 'Baseline', 'ReScene4D']; 776 777 colHeaders.forEach((h, c) => { 778 const cap = document.createElement('div'); 779 cap.className = 'grid-col-caption'; 780 cap.style.gridColumn = c + 1; 781 cap.style.textAlign = 'center'; 782 cap.style.fontWeight = 'bold'; 783 cap.style.fontSize = '1.4rem'; 784 cap.textContent = c === 1 ? `Baseline` : h; 785 grid.appendChild(cap); 786 }); 787 788 // Define grid cells with logical roles 789 const cellOrder = [ 790 { col: 0, row: 1, label: 'tâ', role: 'gt' }, 791 { col: 0, row: 2, label: 'tâ', role: 'gt' }, 792 { col: 1, row: 1, label: 'tâ', role: 'baseline' }, 793 { col: 1, row: 2, label: 'tâ', role: 'baseline' }, 794 { col: 2, row: 1, label: 'tâ', role: 'ours' }, 795 { col: 2, row: 2, label: 'tâ', role: 'ours' } 796 ]; 797 798 const sceneCfg = comparisonScenes[objName] || comparisonScenes.scene_1; 799 800 const viewerConfigs = []; 801 cellOrder.forEach(({ col, row, label, role }) => { 802 const wrap = document.createElement('div'); 803 wrap.style.display = 'flex'; 804 wrap.style.flexDirection = 'column'; 805 wrap.style.minHeight = '0'; 806 wrap.style.gridColumn = col + 1; 807 wrap.style.gridRow = row + 1; 808 809 const rowCap = document.createElement('div'); 810 rowCap.style.fontSize = '1.2rem'; 811 rowCap.style.marginBottom = '4px'; 812 rowCap.textContent = label; 813 wrap.appendChild(rowCap); 814 815 const viewerDiv = document.createElement('div'); 816 viewerDiv.className = 'viewer comparison-cell'; 817 viewerDiv.style.flex = '1'; 818 viewerDiv.style.minHeight = '200px'; 819 viewerDiv.style.borderRadius = '6px'; 820 viewerDiv.style.overflow = 'hidden'; 821 viewerDiv.style.background = '#FCFCFC'; 822 wrap.appendChild(viewerDiv); 823 grid.appendChild(wrap); 824 825 // Determine timestep index (0 -> tâ, 1 -> tâ) 826 const tIndex = (label === 'tâ') ? 0 : 1; 827 const stem = sceneCfg.timesteps[tIndex]; 828 const basePath = `${sceneCfg.pairDir}/${stem}`; 829 830 const rgbUrl = `${basePath}_rgb.ply`; 831 832 let suffix; 833 if (role === 'gt') { 834 suffix = 'gt_instances_filtered'; 835 } else if (role === 'baseline') { 836 suffix = 'mask3d_feature_instances'; 837 } else { 838 // ours / ReScene4D 839 suffix = 'concerto_temporal_sharing_instances'; 840 } 841 const instUrl = `${basePath}_${suffix}.ply`; 842 843 viewerConfigs.push({ viewerDiv, rgbUrl, instUrl }); 844 }); 845 846 vc.appendChild(grid); 847
848 viewerConfigs.forEach(({ viewerDiv, rgbUrl, instUrl }) => { 849 const state = initInputViewer(viewerDiv, { rgbUrl, instUrl }); 850 allStates.push(state); 851 }); 852 inputState = allStates[0]; 853 854 // Sync rotation across all 6 855 allStates.forEach((st) => { 856 if (st.controls) st.controls.autoRotate = true; 857 }); 858 const rotateBtn = document.getElementById('btn-rotate'); 859 if (rotateBtn) rotateBtn.innerText = 'Rotate: On'; 860} 861 862// 10) ADVANCE FRAMES (two sampled viewers in sync) 863function advanceAllFrames(session) { 864 if (session !== currentSession || sampledStates.length === 0) return; 865 if (isPaused) return; 866 867 const globalTotal = slowMode ? (totalFrames * 2 - 1) : totalFrames; 868 const g = currentFrameIdx; 869 const prevG = (g - 1 + globalTotal) % globalTotal; 870 871 sampledStates.forEach((st) => { 872 const prevMap = mapGlobalToLocal(prevG, st); 873 const currMap = mapGlobalToLocal(g, st); 874 875 // Hide whatever was visible at prevG: 876 if (prevMap.isOriginal) { 877 const oldIdx = prevMap.frameIndex; 878 if (st.frameMeshes[oldIdx]) st.frameMeshes[oldIdx].visible = false; 879 } else { 880 const name = `interp_${st.currentSampleId}_${prevMap.frameIndex}_${prevMap.nextIndex}_${prevG}`; 881 const obj = st.scene.getObjectByName(name); 882 if (obj) obj.visible = false; 883 } 884 885 // Show whatever should be visible at g: 886 if (currMap.isOriginal) { 887 const idx = currMap.frameIndex; 888 if (st.frameMeshes[idx]) st.frameMeshes[idx].visible = true; 889 } else { 890 const i0 = currMap.frameIndex; 891 const i1 = currMap.nextIndex; 892 const interpName = `interp_${st.currentSampleId}_${i0}_${i1}_${g}`; 893 let mesh = st.scene.getObjectByName(interpName); 894 if (!mesh) { 895 mesh = buildInterpolatedMeshForState(st, i0, i1, g); 896 } 897 if (mesh) { 898 mesh.visible = true; 899 } 900 } 901 }); 902 903 if (g === globalTotal - 1) { 904 // At the last frame: start prefetching new samples immediately 905 const curr0 = sampledStates[0].currentSampleId; 906 const curr1 = sampledStates[1].currentSampleId; 907 908 let newId0; 909 do { 910 newId0 = sampledStates[0].pickRandom(); 911 } while (newId0 === curr1); 912 913 let newId1; 914 do { 915 newId1 = sampledStates[1].pickRandom(); 916 } while (newId1 === curr0 || newId1 === newId0); 917 918 // Kick off prefetch (showLoading=false) 919 const prefetchPromise0 = sampledStates[0].loadSample(newId0, false); 920 const prefetchPromise1 = sampledStates[1].loadSample(newId1, false); 921 922 // Wait pauseDuration, then swap everything in one go 923 playTimeoutId = setTimeout(async () => { 924 if (session !== currentSession) return; 925 const [res0, res1] = await Promise.all([prefetchPromise0, prefetchPromise1]); 926 if (session !== currentSession) return; 927 928 // For each sampled viewer, remove old trajectories & old meshes before swapping 929 [res0, res1].forEach((res, i) => { 930 const st = sampledStates[i]; 931 if (!res) return; 932 933 // Remove & dispose ALL interpolated meshes (any name starting with 'interp_<oldSampleId>_') 934 const toRemove = []; 935 st.scene.traverse((obj) => { 936 if (obj.name && obj.name.startsWith(`interp_${st.currentSampleId}_`)) { 937 toRemove.push(obj); 938 } 939 }); 940 toRemove.forEach((obj) => { 941 if (obj.geometry) obj.geometry.dispose(); 942 if (obj.material) obj.material.dispose(); 943 st.scene.remove(obj); 944 }); 945 946 // Remove & dispose old frameâmeshes 947 st.frameMeshes.forEach((oldMesh) => { 948 if (oldMesh) { 949 st.scene.remove(oldMesh); 950 oldMesh.geometry.dispose(); 951 oldMesh.material.dispose(); 952 } 953 }); 954 955 // â TRAJ: Remove any trajectory spheres for the old sample 956 st.trajectorySpheres.forEach((sphere) => { 957 if (sphere.geometry) sphere.geometry.dispose(); 958 if (sphere.material) sphere.material.dispose(); 959 st.scene.remove(sphere); 960 }); 961 st.trajectorySpheres.length = 0; 962 963 // Swap in the newly-loaded frames & positions 964 st.frameMeshes = res.newMeshes; 965 st.framePositions = res.newPositions; 966 st.currentSampleId = res.sampleId; 967 968 // Show frame 0 of the new sample 969 if (st.frameMeshes[0]) { 970 st.frameMeshes[0].visible = true; 971 } 972 973 // â TRAJ: If the toggle is ON, immediately draw new spheres for the new sample 974 if (showTrajectories) { 975 drawTrajectoriesForState(st); 976 } 977 }); 978 979 // 7) Restart from frame 0 if still playing 980 if (!isPaused) { 981 currentFrameIdx = 0; 982 advanceAllFrames(session); 983 } 984 }, pauseDuration); 985 } else { 986 // Normal case: simply advance to g+1 after frameInterval 987 playTimeoutId = setTimeout(() => { 988 if (session !== currentSession) return; 989 if (!isPaused) { 990 currentFrameIdx = (currentFrameIdx + 1) % globalTotal; 991 advanceAllFrames(session); 992 } 993 }, frameInterval); 994 } 995 996 // --- helper to map âglobal frame indexâ â {isOriginal, frameIndex, nextIndex, alpha} --- 997 function mapGlobalToLocal(globalIdx, st) { 998 if (!slowMode) {
999 return { isOriginal: true, frameIndex: globalIdx, alpha: 0 }; 1000 } else { 1001 if (globalIdx % 2 === 0) { 1002 return { isOriginal: true, frameIndex: globalIdx / 2, alpha: 0 }; 1003 } else { 1004 const i0 = Math.floor(globalIdx / 2); 1005 const i1 = i0 + 1; 1006 return { isOriginal: false, frameIndex: i0, nextIndex: i1, alpha: 0.5 }; 1007 } 1008 } 1009 } 1010 1011 // Helper: build (and name) an interpolated mesh in `st` for indices (i0, i1) (unchanged) 1012 function buildInterpolatedMeshForState(st, i0, i1, globalIdx) { 1013 const name = `interp_${st.currentSampleId}_${i0}_${i1}_${globalIdx}`; 1014 const pos0 = st.framePositions[i0]; 1015 const pos1 = st.framePositions[i1]; 1016 if (!pos0 || !pos1) return null; 1017 1018 const sphereGeo = new THREE.SphereGeometry(0.01, 6, 4); 1019 const mat = new THREE.MeshStandardMaterial({ 1020 transparent: true, 1021 metalness: 0.2, 1022 roughness: 0.4, 1023 depthWrite: true, 1024 flatShading: false 1025 }); 1026 const mesh = new THREE.InstancedMesh(sphereGeo, mat, st.N_points); 1027 mesh.castShadow = true; 1028 mesh.receiveShadow = false; 1029 mesh.name = name; 1030 1031 const dummyMatrix = new THREE.Matrix4(); 1032 const color = new THREE.Color(); 1033 const srcMesh = st.frameMeshes[i0]; 1034 1035 for (let i = 0; i < st.N_points; i++) { 1036 const x0 = pos0[3 * i], y0 = pos0[3 * i + 1], z0 = pos0[3 * i + 2]; 1037 const x1 = pos1[3 * i], y1 = pos1[3 * i + 1], z1 = pos1[3 * i + 2]; 1038 const xm = 0.5 * (x0 + x1), ym = 0.5 * (y0 + y1), zm = 0.5 * (z0 + z1); 1039 dummyMatrix.makeTranslation(xm, ym, zm); 1040 mesh.setMatrixAt(i, dummyMatrix); 1041 1042 if (srcMesh.instanceColor) { 1043 const cA = srcMesh.instanceColor; 1044 const r = cA.getX(i), g = cA.getY(i), b = cA.getZ(i); 1045 color.setRGB(r, g, b); 1046 } else { 1047 color.setRGB(0.5, 0.5, 0.5); 1048 } 1049 mesh.setColorAt(i, color); 1050 } 1051 1052 mesh.visible = false; 1053 st.scene.add(mesh); 1054 return mesh; 1055 } 1056} 1057 1058// 11) RENDER LOOP (calls controls.update + render each scene) (unchanged) 1059function animateAll() { 1060 requestAnimationFrame(animateAll); 1061 allStates.forEach((st) => { 1062 st.controls.update(); 1063 st.renderer.render(st.scene, st.camera); 1064 }); 1065} 1066 1067// 12) DRAW TRAJECTORIES FOR ONE VIEWER STATE (sampled viewer only) â TRAJ 1068function drawTrajectoriesForState(st) { 1069 const N = st.N_points; 1070 if (N === 0 || !st.framePositions[0]) return; 1071 1072 // 1) Pick 5% of indices at random 1073 const numToSample = Math.max(1, Math.floor(N * 0.075)); 1074 const indices = new Set(); 1075 while (indices.size < numToSample) { 1076 indices.add(Math.floor(Math.random() * N)); 1077 } 1078 const selectedIndices = Array.from(indices); 1079 1080 // 2) We will have totalInstances = numToSample * totalFrames 1081 const totalInstances = numToSample * totalFrames; 1082 const instancedMesh = new THREE.InstancedMesh( 1083 _trajSphereGeo, 1084 _trajSphereMat, 1085 totalInstances 1086 ); 1087 // Enable perâinstance color 1088 instancedMesh.instanceColor = new THREE.InstancedBufferAttribute( 1089 new Float32Array(totalInstances * 3), 1090 3 1091 ); 1092 instancedMesh.count = totalInstances; // yep, exactly this many 1093 instancedMesh.castShadow = false; 1094 instancedMesh.receiveShadow = false; 1095 1096 // 3) Fill in each instanceâs matrix and color 1097 const dummyMatrix = new THREE.Matrix4(); 1098 const color = new THREE.Color(); 1099 1100 let instanceIdx = 0; 1101 for (let pi = 0; pi < selectedIndices.length; pi++) { 1102 const ptIdx = selectedIndices[pi]; 1103 1104 // Preâgrab colorSeries[ptIdx] for all frames if you wantâthis avoids 1105 // reâreading instanceColor on each frame. But we can just read from meshAtT.instanceColor as below. 1106 for (let t = 0; t < totalFrames; t++) { 1107 const posArr = st.framePositions[t]; // Float32Array length=N*3 1108 const x = posArr[3 * ptIdx]; 1109 const y = posArr[3 * ptIdx + 1]; 1110 const z = posArr[3 * ptIdx + 2]; 1111 1112 // Build translation for this instance 1113 dummyMatrix.makeTranslation(x, y, z); 1114 instancedMesh.setMatrixAt(instanceIdx, dummyMatrix); 1115 1116 // Determine the color from the original instancedâmesh at frame t 1117 const meshAtT = st.frameMeshes[t]; 1118 if (meshAtT.instanceColor) { 1119 const cA = meshAtT.instanceColor; 1120 const r = cA.getX(ptIdx); 1121 const g = cA.getY(ptIdx); 1122 const b = cA.getZ(ptIdx); 1123 color.setRGB(r, g, b); 1124 } else { 1125 color.setRGB(0.5, 0.5, 0.5); 1126 } 1127 instancedMesh.setColorAt(instanceIdx, color); 1128 1129 instanceIdx++; 1130 } 1131 } 1132 1133 // Make sure the InstancedMesh knows its colors were updated
1134 instancedMesh.instanceColor.needsUpdate = true; 1135 instancedMesh.instanceMatrix.needsUpdate = true; 1136 1137 // 4) Add the single InstancedMesh to the scene 1138 st.scene.add(instancedMesh); 1139 1140 // 5) Push it into st.trajectorySpheres so the teardown logic still works 1141 // (Later, removeTrajectoriesForState can simply loop over trajectorySpheres, 1142 // dispose geometry/material, and scene.remove(obj).) 1143 st.trajectorySpheres.push(instancedMesh); 1144} 1145 1146// 13) REMOVE ALL TRAJECTORIES FOR ONE VIEWER STATE â TRAJ 1147function removeTrajectoriesForState(st) { 1148 st.trajectorySpheres.forEach((sphere) => { 1149 if (sphere.geometry) sphere.geometry.dispose(); 1150 if (sphere.material) sphere.material.dispose(); 1151 st.scene.remove(sphere); 1152 }); 1153 st.trajectorySpheres.length = 0; 1154} 1155 1156// 14) Only build the 3x2 comparison grid (GT | Baseline | Ours x t0, t1). No other layout. 1157function selectObject(objName) { 1158 buildComparisonGrid(objName); 1159} 1160 1161// 15) HOOK UP TAB BUTTONS + BASELINE + ROTATE ONCE DOM IS READY 1162window.addEventListener('DOMContentLoaded', () => { 1163 // Tab buttons: 1164 document.querySelectorAll('.tab-button').forEach((btn) => { 1165 btn.addEventListener('click', () => { 1166 selectObject(btn.dataset.obj); 1167 }); 1168 }); 1169 1170 // âRotateâ button toggles autoRotate: 1171 const rotateBtn = document.getElementById('btn-rotate'); 1172 if (rotateBtn) rotateBtn.addEventListener('click', () => { 1173 const anyAuto = allStates.some((st) => st.controls?.autoRotate); 1174 if (anyAuto) { 1175 allStates.forEach((st) => { 1176 if (st.controls) st.controls.autoRotate = false; 1177 }); 1178 } else { 1179 allStates.forEach((st) => { 1180 if (st.controls) st.controls.autoRotate = true; 1181 }); 1182 } 1183 // Enable the button again (in case it was disabled during drag) 1184 rotateBtn.disabled = false; 1185 1186 const anyAuto2 = allStates.some((st) => st.controls?.autoRotate); 1187 rotateBtn.innerText = anyAuto2 1188 ? 'Rotate: On' 1189 : 'Rotate: Off'; 1190 }); 1191 1192 // âShow Trajectoriesâ button (toggle): 1193 const trajBtn = document.getElementById('btn-traj'); 1194 if (trajBtn) { 1195 trajBtn.innerText = 'Trajectories: Off'; // initially off 1196 trajBtn.addEventListener('click', () => { 1197 showTrajectories = !showTrajectories; 1198 if (showTrajectories) { 1199 trajBtn.innerText = 'Trajectories: On'; 1200 // Draw trajectories for each sampled viewer if frames are already loaded: 1201 sampledStates.forEach((st) => { 1202 const allLoaded = st.framePositions.every((arr) => arr !== null); 1203 if (allLoaded) { 1204 drawTrajectoriesForState(st); 1205 } 1206 }); 1207 } else { 1208 trajBtn.innerText = 'Trajectories: Off'; 1209 // Remove any existing trajectories 1210 sampledStates.forEach((st) => { 1211 removeTrajectoriesForState(st); 1212 }); 1213 } 1214 }); 1215 } 1216 1217 // â UPDATED: âPlay/Pauseâ button wiring (now also stops/starts autoRotate) 1218 const pauseBtn = document.getElementById('btn-pause'); 1219 if (pauseBtn) { 1220 pauseBtn.innerText = 'Pause'; // default: playing 1221 pauseBtn.addEventListener('click', () => { 1222 isPaused = !isPaused; 1223 1224 if (isPaused) { 1225 // 1) Switch button label 1226 pauseBtn.innerText = 'Play'; 1227 // 2) Stop the frameâadvance loop 1228 clearTimeout(playTimeoutId); 1229 // 3) Force all viewers to stop rotating 1230 allStates.forEach((st) => { 1231 if (st.controls) st.controls.autoRotate = false; 1232 }); 1233 } else { 1234 // 1) Switch button label 1235 pauseBtn.innerText = 'Pause'; 1236 // 2) Restore autoRotate only if "Rotate" is currently set to On: 1237 const rotateIsOn = rotateBtn.innerText.includes('On'); 1238 allStates.forEach((st) => { 1239 if (st.controls) st.controls.autoRotate = rotateIsOn; 1240 }); 1241 // 3) Reâstart frameâadvance from the current frame 1242 advanceAllFrames(currentSession); 1243 } 1244 }); 1245 } 1246 1247 // Show Scene 2 by default 1248 selectObject('scene_2'); 1249 1250 // Start the render loop 1251 animateAll(); 1252});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.