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.
410 lines
16 KiB
HTML
410 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>GeomPack PBR Viewer</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: #1a1a1a;
|
|
font-family: monospace;
|
|
}
|
|
#canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
#loading {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
}
|
|
#info {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
left: 10px;
|
|
color: #888;
|
|
font-size: 11px;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="canvas"></canvas>
|
|
<div id="loading">Ready to load mesh</div>
|
|
<div id="info">PBR Viewer</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 { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
|
|
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
|
|
|
|
console.log('[GeomPack PBR Viewer] Initializing...');
|
|
|
|
const canvas = document.getElementById('canvas');
|
|
const loading = document.getElementById('loading');
|
|
const info = document.getElementById('info');
|
|
|
|
// Scene
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x1a1a1a);
|
|
|
|
// Camera
|
|
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 1000);
|
|
camera.position.set(2, 2, 2);
|
|
|
|
// Renderer with PBR settings
|
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
|
|
// Critical PBR settings
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1.2;
|
|
renderer.sortObjects = true; // Enable proper transparency sorting
|
|
|
|
// Controls
|
|
const controls = new OrbitControls(camera, canvas);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.zoomSpeed = 3.0;
|
|
|
|
// Grid (subtle)
|
|
const grid = new THREE.GridHelper(10, 20, 0x333333, 0x222222);
|
|
scene.add(grid);
|
|
|
|
// Send camera state to parent node widget
|
|
function sendCameraState() {
|
|
window.parent.postMessage({
|
|
type: 'WIDGET_UPDATE',
|
|
widget: 'camera_state',
|
|
value: JSON.stringify({
|
|
position: [camera.position.x, camera.position.y, camera.position.z],
|
|
focalPoint: [controls.target.x, controls.target.y, controls.target.z],
|
|
viewUp: [camera.up.x, camera.up.y, camera.up.z]
|
|
})
|
|
}, '*');
|
|
}
|
|
|
|
function applyCameraState(cameraState) {
|
|
if (!cameraState) return false;
|
|
try {
|
|
const cs = typeof cameraState === 'string' ? JSON.parse(cameraState) : cameraState;
|
|
if (!cs.position || !cs.focalPoint || !cs.viewUp) return false;
|
|
camera.position.set(...cs.position);
|
|
controls.target.set(...cs.focalPoint);
|
|
camera.up.set(...cs.viewUp);
|
|
camera.updateProjectionMatrix();
|
|
controls.update();
|
|
return true;
|
|
} catch(e) { return false; }
|
|
}
|
|
|
|
controls.addEventListener('end', sendCameraState);
|
|
|
|
let _pendingCameraState = null;
|
|
|
|
// Current mesh
|
|
let currentMesh = null;
|
|
let envMapLoaded = false;
|
|
|
|
// Get the extension folder path from the current URL
|
|
const getExtensionPath = () => {
|
|
const url = window.location.href;
|
|
const match = url.match(/\/extensions\/([^/]+)\//);
|
|
return match ? `/extensions/${match[1]}` : '/extensions/comfyui-geometrypack';
|
|
};
|
|
|
|
// Load environment map for PBR reflections
|
|
const loadEnvironment = () => {
|
|
return new Promise((resolve, reject) => {
|
|
const extPath = getExtensionPath();
|
|
const exrLoader = new EXRLoader();
|
|
|
|
const exrPath = `${extPath}/assets/studio.exr`;
|
|
console.log('[GeomPack PBR Viewer] Loading environment map from:', exrPath);
|
|
|
|
exrLoader.load(
|
|
exrPath,
|
|
(texture) => {
|
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
scene.environment = texture;
|
|
envMapLoaded = true;
|
|
console.log('[GeomPack PBR Viewer] Environment map Loaded successfully');
|
|
resolve(texture);
|
|
},
|
|
(progress) => {
|
|
if (progress.lengthComputable) {
|
|
console.log('[GeomPack PBR Viewer] Env map loading:', Math.round(progress.loaded / progress.total * 100) + '%');
|
|
}
|
|
},
|
|
(error) => {
|
|
console.error('[GeomPack PBR Viewer] Failed to load EXR environment:', error);
|
|
console.warn('[GeomPack PBR Viewer] Using fallback gradient environment');
|
|
// Fallback: create a simple gradient environment
|
|
createFallbackEnvironment();
|
|
resolve(null);
|
|
}
|
|
);
|
|
});
|
|
};
|
|
|
|
// Fallback environment if EXR fails to load
|
|
const createFallbackEnvironment = () => {
|
|
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
|
pmremGenerator.compileEquirectangularShader();
|
|
|
|
// Create a simple gradient sky
|
|
const skyGeo = new THREE.SphereGeometry(500, 32, 15);
|
|
const skyMat = new THREE.ShaderMaterial({
|
|
uniforms: {
|
|
topColor: { value: new THREE.Color(0x444466) },
|
|
bottomColor: { value: new THREE.Color(0x222222) },
|
|
offset: { value: 33 },
|
|
exponent: { value: 0.6 }
|
|
},
|
|
vertexShader: `
|
|
varying vec3 vWorldPosition;
|
|
void main() {
|
|
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
vWorldPosition = worldPosition.xyz;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
uniform vec3 topColor;
|
|
uniform vec3 bottomColor;
|
|
uniform float offset;
|
|
uniform float exponent;
|
|
varying vec3 vWorldPosition;
|
|
void main() {
|
|
float h = normalize(vWorldPosition + offset).y;
|
|
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
|
|
}
|
|
`,
|
|
side: THREE.BackSide
|
|
});
|
|
|
|
const sky = new THREE.Mesh(skyGeo, skyMat);
|
|
|
|
// Generate environment map from sky
|
|
const envScene = new THREE.Scene();
|
|
envScene.add(sky);
|
|
const envMap = pmremGenerator.fromScene(envScene).texture;
|
|
scene.environment = envMap;
|
|
envMapLoaded = true;
|
|
|
|
pmremGenerator.dispose();
|
|
console.log('[GeomPack PBR Viewer] Using fallback gradient environment');
|
|
};
|
|
|
|
// Lighting setup matching standard ComfyUI Load3D viewer
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
|
scene.add(ambientLight);
|
|
|
|
// Main light
|
|
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
mainLight.position.set(0, 10, 10);
|
|
scene.add(mainLight);
|
|
|
|
// Back light
|
|
const backLight = new THREE.DirectionalLight(0xffffff, 0.5);
|
|
backLight.position.set(0, 10, -10);
|
|
scene.add(backLight);
|
|
|
|
// Left fill light
|
|
const leftFillLight = new THREE.DirectionalLight(0xffffff, 0.3);
|
|
leftFillLight.position.set(-10, 0, 0);
|
|
scene.add(leftFillLight);
|
|
|
|
// Right fill light
|
|
const rightFillLight = new THREE.DirectionalLight(0xffffff, 0.3);
|
|
rightFillLight.position.set(10, 0, 0);
|
|
scene.add(rightFillLight);
|
|
|
|
// Bottom light
|
|
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.2);
|
|
bottomLight.position.set(0, -10, 0);
|
|
scene.add(bottomLight);
|
|
|
|
// Animation loop
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
animate();
|
|
|
|
// Handle window resize
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
// Load mesh function
|
|
async function loadMesh(filepath) {
|
|
console.log('[GeomPack PBR Viewer] Loading:', filepath);
|
|
loading.textContent = 'Loading mesh...';
|
|
loading.style.display = 'block';
|
|
|
|
// Ensure environment is loaded first
|
|
if (!envMapLoaded) {
|
|
loading.textContent = 'Loading environment...';
|
|
await loadEnvironment();
|
|
}
|
|
|
|
// Remove old mesh
|
|
if (currentMesh) {
|
|
scene.remove(currentMesh);
|
|
currentMesh.traverse((child) => {
|
|
if (child.geometry) child.geometry.dispose();
|
|
if (child.material) {
|
|
if (Array.isArray(child.material)) {
|
|
child.material.forEach(m => m.dispose());
|
|
} else {
|
|
child.material.dispose();
|
|
}
|
|
}
|
|
});
|
|
currentMesh = null;
|
|
}
|
|
|
|
loading.textContent = 'Loading mesh...';
|
|
const loader = new GLTFLoader();
|
|
|
|
loader.load(
|
|
filepath,
|
|
(gltf) => {
|
|
currentMesh = gltf.scene;
|
|
|
|
// Center mesh
|
|
const box = new THREE.Box3().setFromObject(currentMesh);
|
|
const center = box.getCenter(new THREE.Vector3());
|
|
currentMesh.position.sub(center);
|
|
|
|
// Configure materials for PBR
|
|
let meshCount = 0;
|
|
let hasTextures = false;
|
|
currentMesh.traverse((child) => {
|
|
if (child.isMesh) {
|
|
meshCount++;
|
|
|
|
// Enable environment map on all materials
|
|
if (child.material) {
|
|
const materials = Array.isArray(child.material) ? child.material : [child.material];
|
|
materials.forEach(mat => {
|
|
if (mat.isMeshStandardMaterial || mat.isMeshPhysicalMaterial) {
|
|
mat.envMapIntensity = 1.5;
|
|
|
|
// Enable transparency support
|
|
// Check if baseColorTexture has alpha channel
|
|
const hasAlphaMap = mat.map && mat.map.format === THREE.RGBAFormat;
|
|
const hasAlphaTest = mat.alphaTest > 0;
|
|
const needsTransparency = mat.transparent || mat.opacity < 1.0 || hasAlphaMap;
|
|
|
|
if (needsTransparency || hasAlphaMap) {
|
|
mat.transparent = true;
|
|
mat.alphaTest = 0.01; // Discard nearly-transparent pixels
|
|
mat.side = THREE.DoubleSide; // Render both sides
|
|
mat.depthWrite = true; // Keep depth writing for proper sorting
|
|
}
|
|
|
|
mat.needsUpdate = true;
|
|
|
|
// Check if has textures
|
|
if (mat.map || mat.metalnessMap || mat.roughnessMap) {
|
|
hasTextures = true;
|
|
}
|
|
|
|
console.log('[GeomPack PBR Viewer] Material:', {
|
|
name: mat.name,
|
|
metalness: mat.metalness,
|
|
roughness: mat.roughness,
|
|
opacity: mat.opacity,
|
|
transparent: mat.transparent,
|
|
alphaTest: mat.alphaTest,
|
|
hasMap: !!mat.map,
|
|
mapFormat: mat.map ? mat.map.format : 'none',
|
|
hasMetalnessMap: !!mat.metalnessMap,
|
|
hasRoughnessMap: !!mat.roughnessMap,
|
|
hasAlphaMap: hasAlphaMap
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add to scene
|
|
scene.add(currentMesh);
|
|
|
|
// Adjust camera (restore saved state or auto-position)
|
|
const size = box.getSize(new THREE.Vector3());
|
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
|
|
camera.near = maxDim * 0.001;
|
|
camera.far = maxDim * 100;
|
|
camera.updateProjectionMatrix();
|
|
|
|
if (!applyCameraState(_pendingCameraState)) {
|
|
const distance = maxDim * 2.0;
|
|
camera.position.set(distance * 0.7, distance * 0.5, distance * 0.7);
|
|
camera.lookAt(0, 0, 0);
|
|
controls.target.set(0, 0, 0);
|
|
controls.update();
|
|
}
|
|
_pendingCameraState = null;
|
|
|
|
loading.style.display = 'none';
|
|
info.textContent = `PBR Viewer | ${meshCount} mesh(es) | Textures: ${hasTextures ? 'Yes' : 'No'}`;
|
|
console.log('[GeomPack PBR Viewer] Mesh Loaded successfully');
|
|
},
|
|
(progress) => {
|
|
if (progress.lengthComputable) {
|
|
const percent = (progress.loaded / progress.total * 100).toFixed(0);
|
|
loading.textContent = `Loading mesh... ${percent}%`;
|
|
}
|
|
},
|
|
(error) => {
|
|
console.error('[GeomPack PBR Viewer] Error loading mesh:', error);
|
|
loading.textContent = 'Error loading mesh - check console';
|
|
loading.style.color = '#ff6666';
|
|
}
|
|
);
|
|
}
|
|
|
|
// Listen for messages from parent window
|
|
window.addEventListener('message', (event) => {
|
|
console.log('[GeomPack PBR Viewer] Received message:', event.data);
|
|
if (event.data.type === 'LOAD_MESH') {
|
|
_pendingCameraState = event.data.cameraState || null;
|
|
loadMesh(event.data.filepath);
|
|
}
|
|
});
|
|
|
|
// Pre-load environment
|
|
loadEnvironment().then(() => {
|
|
console.log('[GeomPack PBR Viewer] Ready');
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|