Files
PozzettiAndrea-ComfyUI-Geom…/javascript/viewer_compare_smpl_bvh.html
T
PozzettiAndrea 414f759791 Vendor frontend JS into javascript/; drop the prestartup copy dance
The viewer JS/HTML/bundles are now committed in javascript/ (declared via
pyproject [tool.comfy] web = "javascript") instead of being copied from the
shared comfy-3d-viewers pip package at prestartup. Consequences:

- requirements.txt is now just comfy-env (host env installs nothing else,
  per the never-touch-the-host-env principle)
- prestartup_script.py no longer copies viewers or web_src; it keeps only
  setup_env() and the input-asset copies
- WEB_DIRECTORY attribute removed; the pyproject declaration is the single
  source (served at /extensions/comfyui-geometrypack/, keyed by project name)
- web_src/ deleted; its 6 widgets now live in javascript/js/ as the tracked
  source, edited directly
- viewer_pbr.html hardcoded-fallback URL updated to the new mount name

Iframe URLs auto-detect the mount from import.meta.url, so the folder rename
and mount-name change are transparent at runtime.
2026-08-12 17:37:09 +02:00

355 lines
12 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; background: #1a1a1a; font-family: Arial, sans-serif; }
#container { display: flex; flex-direction: column; width: 100%; height: 100%; }
#canvas-container { flex: 1; position: relative; overflow: hidden; width: 100%; min-height: 0; background: #000; }
#controls {
height: 40px;
flex-shrink: 0;
background: #252525;
display: flex;
align-items: center;
padding: 0 10px;
gap: 10px;
border-top: 1px solid #333;
z-index: 200;
}
.panel-label {
position: absolute;
top: 10px;
color: rgba(255, 255, 255, 0.7);
font-weight: bold;
font-size: 14px;
pointer-events: none;
background: rgba(0,0,0,0.5);
padding: 4px 8px;
border-radius: 4px;
z-index: 10;
}
#label-smpl { left: 10px; }
#label-bvh { left: 50%; margin-left: 10px; }
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
background: rgba(0,0,0,0.8);
padding: 15px 20px;
border-radius: 8px;
z-index: 100;
}
button {
background: #4a9eff;
border: none;
color: white;
border-radius: 4px;
width: 30px;
height: 30px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
button:hover { background: #3a8eef; }
button:disabled { background: #444; cursor: default; }
input[type=range] {
flex-grow: 1;
height: 6px;
background: #444;
border-radius: 3px;
outline: none;
}
.frame-counter {
color: #aaa;
font-family: monospace;
font-size: 12px;
min-width: 80px;
text-align: right;
}
</style>
</head>
<body>
<div id="container">
<div id="canvas-container">
<div id="loading">Waiting for data...</div>
<div id="label-smpl" class="panel-label">SMPL (Mesh)</div>
<div id="label-bvh" class="panel-label">BVH (Skeleton)</div>
</div>
<div id="controls">
<button id="play-btn">&#9654;</button>
<input type="range" id="slider" min="0" max="0" value="0" step="1">
<div class="frame-counter" id="frame-display">0 / 0</div>
</div>
</div>
<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>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { BVHLoader } from 'three/addons/loaders/BVHLoader.js';
let renderer, camera, controls;
let sceneSMPL, sceneBVH;
let smplData = null;
let smplMesh = null;
let bvhHelper = null;
let bvhMixer = null;
let bvhAction = null;
let bvhSkeleton = null;
let isPlaying = false;
let currentFrame = 0;
let totalFrames = 0;
let fps = 30;
let clock = new THREE.Clock();
const slider = document.getElementById('slider');
const frameDisplay = document.getElementById('frame-display');
const playBtn = document.getElementById('play-btn');
function init() {
const container = document.getElementById('canvas-container');
const w = container.clientWidth;
const h = container.clientHeight;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(w, h);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setScissorTest(true);
container.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(50, w / h, 0.01, 100);
camera.position.set(0, 1.5, 4);
controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1, 0);
controls.enableDamping = true;
sceneSMPL = new THREE.Scene();
sceneSMPL.background = new THREE.Color(0x111111);
setupSceneCommon(sceneSMPL);
sceneBVH = new THREE.Scene();
sceneBVH.background = new THREE.Color(0x1a1a1a);
setupSceneCommon(sceneBVH);
window.addEventListener('resize', onWindowResize);
window.addEventListener('message', handleMessage);
playBtn.onclick = togglePlay;
slider.oninput = (e) => {
pause();
seek(parseInt(e.target.value));
};
animate();
window.parent.postMessage({ type: 'VIEWER_READY' }, '*');
}
function setupSceneCommon(scene) {
const grid = new THREE.GridHelper(10, 10, 0x444444, 0x222222);
scene.add(grid);
scene.add(new THREE.AxesHelper(0.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(2, 5, 5);
scene.add(dirLight);
const ambLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambLight);
}
function onWindowResize() {
const container = document.getElementById('canvas-container');
const w = container.clientWidth;
const h = container.clientHeight;
if (w === 0 || h === 0) return;
camera.aspect = (w * 0.5) / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
}
function togglePlay() {
if (isPlaying) pause(); else play();
}
function play() {
isPlaying = true;
playBtn.innerHTML = "&#9208;";
clock.start();
}
function pause() {
isPlaying = false;
playBtn.innerHTML = "&#9654;";
}
function seek(frame) {
currentFrame = Math.max(0, Math.min(frame, totalFrames - 1));
updateVisuals(currentFrame);
updateUI();
}
function updateUI() {
slider.value = Math.floor(currentFrame);
frameDisplay.textContent = Math.floor(currentFrame) + ' / ' + totalFrames;
}
function updateVisuals(frame) {
const time = frame / fps;
if (smplMesh && smplData) {
const f = Math.min(Math.floor(frame), smplData.numFrames - 1);
const start = f * smplData.numVerts * 3;
const end = start + smplData.numVerts * 3;
const frameVerts = smplData.vertices.subarray(start, end);
smplMesh.geometry.attributes.position.array.set(frameVerts);
smplMesh.geometry.attributes.position.needsUpdate = true;
smplMesh.geometry.computeVertexNormals();
}
if (bvhMixer) {
bvhMixer.setTime(time);
}
}
function animate() {
requestAnimationFrame(animate);
controls.update();
if (isPlaying && totalFrames > 0) {
const delta = clock.getDelta();
currentFrame += delta * fps;
if (currentFrame >= totalFrames) currentFrame = 0;
updateVisuals(currentFrame);
updateUI();
}
renderSplitScreen();
}
function renderSplitScreen() {
const container = document.getElementById('canvas-container');
const w = container.clientWidth;
const h = container.clientHeight;
if (w === 0 || h === 0) return;
renderer.setViewport(0, 0, w / 2, h);
renderer.setScissor(0, 0, w / 2, h);
renderer.render(sceneSMPL, camera);
renderer.setViewport(w / 2, 0, w / 2, h);
renderer.setScissor(w / 2, 0, w / 2, h);
renderer.render(sceneBVH, camera);
}
async function handleMessage(event) {
const data = event.data;
if (data.type === 'loadData') {
await loadAllData(data.smplFilename, data.bvhContent, data.smplMeshApiPath);
}
}
async function loadAllData(smplFilename, bvhContent, smplMeshApiPath) {
const loading = document.getElementById('loading');
loading.style.display = 'block';
loading.textContent = "Loading data...";
pause();
currentFrame = 0;
totalFrames = 0;
try {
if (smplFilename) {
const apiPath = smplMeshApiPath || '/motioncapture/smpl_mesh';
const res = await fetch(apiPath + '?filename=' + smplFilename);
const buf = await res.arrayBuffer();
parseSMPL(buf);
}
if (bvhContent) {
parseBVH(bvhContent);
}
slider.max = totalFrames - 1;
updateVisuals(0);
updateUI();
loading.style.display = 'none';
onWindowResize();
} catch (e) {
loading.textContent = "Error: " + e.message;
}
}
function parseSMPL(buffer) {
const dv = new DataView(buffer);
let offset = 4;
const numFrames = dv.getUint32(offset, true); offset += 4;
const numVerts = dv.getUint32(offset, true); offset += 4;
const numFaces = dv.getUint32(offset, true); offset += 4;
fps = dv.getFloat32(offset, true); offset += 4;
const verts = new Float32Array(buffer, offset, numFrames * numVerts * 3);
offset += numFrames * numVerts * 3 * 4;
const faces = new Uint32Array(buffer, offset, numFaces * 3);
smplData = { vertices: verts, faces: faces, numFrames, numVerts };
if (smplMesh) sceneSMPL.remove(smplMesh);
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(verts.subarray(0, numVerts*3), 3));
geo.setIndex(new THREE.BufferAttribute(faces, 1));
geo.computeVertexNormals();
const mat = new THREE.MeshPhongMaterial({ color: 0x4a9eff, flatShading: false, shininess: 30, side: THREE.DoubleSide });
smplMesh = new THREE.Mesh(geo, mat);
sceneSMPL.add(smplMesh);
totalFrames = Math.max(totalFrames, numFrames);
}
function parseBVH(content) {
const loader = new BVHLoader();
const result = loader.parse(content);
if (bvhHelper) sceneBVH.remove(bvhHelper);
if (bvhSkeleton) sceneBVH.remove(bvhSkeleton);
bvhSkeleton = result.skeleton.bones[0];
bvhHelper = new THREE.SkeletonHelper(bvhSkeleton);
bvhHelper.material.color.setHex(0x00ff00);
bvhHelper.material.linewidth = 3;
sceneBVH.add(bvhSkeleton);
sceneBVH.add(bvhHelper);
bvhMixer = new THREE.AnimationMixer(bvhSkeleton);
bvhAction = bvhMixer.clipAction(result.clip);
bvhAction.play();
const bvhFrames = Math.floor(result.clip.duration * fps);
totalFrames = Math.max(totalFrames, bvhFrames);
}
init();
console.log('[Compare SMPL BVH Viewer] Loaded successfully');
</script>
</body>
</html>