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.
355 lines
12 KiB
HTML
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">▶</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 = "⏸";
|
|
clock.start();
|
|
}
|
|
|
|
function pause() {
|
|
isPlaying = false;
|
|
playBtn.innerHTML = "▶";
|
|
}
|
|
|
|
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>
|