HunyuanWorld-Demo / modelviewer.html
mooki0's picture
Initial commit of Gradio app
57276d4 verified
<!DOCTYPE html>
<html>
<head>
<title>Simple PLY Viewer</title>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
background: #222;
}
canvas {
display: block;
width: 100%;
height: 80vh;
}
#upload-container {
padding: 20px;
background: #333;
border-bottom: 1px solid #444;
color: white;
}
#file-input {
display: none;
}
.upload-btn {
background: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.upload-btn:hover {
background: #45a049;
}
#loading {
display: none;
margin-top: 10px;
color: #aaa;
}
#controls {
padding: 10px;
background: #333;
color: white;
}
.control-btn {
padding: 8px 12px;
margin-right: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
background: #555;
color: white;
}
.control-btn:hover {
background: #666;
}
#instructions {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
font-size: 14px;
}
</style>
</head>
<body>
<div id="upload-container">
<label for="file-input" class="upload-btn">Select PLY/DRC Files</label>
<input id="file-input" type="file" accept=".ply,.drc" multiple>
<div id="loading">Loading...</div>
</div>
<div id="controls">
<button id="rotate-toggle" class="control-btn">Pause Rotation</button>
<button id="reset-view" class="control-btn">Reset View</button>
</div>
<div id="instructions">
Controls: WASD to move, Mouse drag to look around
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/PLYLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/DRACOLoader.js"></script>
<script>
// Initialize scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true // 允许透明背景
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Movement variables
const moveSpeed = 0.01;
const maxDistance = 0.3; // Maximum movement distance from origin
const keys = {
w: false,
a: false,
s: false,
d: false
};
let isMouseDown = false;
let previousMousePosition = {
x: 0,
y: 0
};
let rotationSpeed = 0.0005;
let isRotating = true;
let sceneCenter = new THREE.Vector3();
let animationId = null;
let isAtLimit = false;
// Event listeners for keyboard controls
document.addEventListener('keydown', (event) => {
switch (event.key.toLowerCase()) {
case 'w': keys.w = true; break;
case 'a': keys.a = true; break;
case 's': keys.s = true; break;
case 'd': keys.d = true; break;
}
// Restart animation if it was stopped
if (!animationId && (keys.w || keys.a || keys.s || keys.d)) {
animate();
}
});
document.addEventListener('keyup', (event) => {
switch (event.key.toLowerCase()) {
case 'w': keys.w = false; break;
case 'a': keys.a = false; break;
case 's': keys.s = false; break;
case 'd': keys.d = false; break;
}
});
// Mouse drag for camera-relative rotation
renderer.domElement.addEventListener('mousedown', (event) => {
isMouseDown = true;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
// Prevent default to avoid text selection
event.preventDefault();
});
document.addEventListener('mouseup', () => {
isMouseDown = false;
});
document.addEventListener('mousemove', (event) => {
if (isMouseDown) {
const deltaMove = {
x: event.clientX - previousMousePosition.x,
y: event.clientY - previousMousePosition.y
};
const quaternion = new THREE.Quaternion();
// Horizontal rotation (Y-axis)
const yQuaternion = new THREE.Quaternion();
yQuaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), -deltaMove.x * 0.002);
quaternion.multiply(yQuaternion);
// Vertical rotation (X-axis)
const xQuaternion = new THREE.Quaternion();
xQuaternion.setFromAxisAngle(new THREE.Vector3(1, 0, 0), -deltaMove.y * 0.002);
quaternion.multiply(xQuaternion);
// Apply rotation
camera.quaternion.multiply(quaternion);
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
});
// Prevent context menu on canvas
renderer.domElement.addEventListener('contextmenu', (event) => {
event.preventDefault();
});
// initialize all loader
const loader = new THREE.PLYLoader();
const dracoLoader = new THREE.DRACOLoader();
dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/libs/draco/');
let loadedCount = 0;
let totalFiles = 0;
// File upload handler
document.getElementById('file-input').addEventListener('change', function(e) {
const files = e.target.files;
if (files.length === 0) return;
document.getElementById('loading').style.display = 'block';
totalFiles = files.length;
loadedCount = 0;
// Clear existing models from scene
scene.children.slice().forEach(child => {
if (child instanceof THREE.Mesh) {
if (child.geometry) child.geometry.dispose();
if (child.material) child.material.dispose();
scene.remove(child);
}
});
// Load each PLY file
Array.from(files).forEach((file, index) => {
const reader = new FileReader();
reader.onload = function(event) {
try {
if (file.name.endsWith('.ply')) {
const geometry = loader.parse(event.target.result);
const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
vertexColors: true,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.rotateX(-Math.PI / 2);
mesh.rotateZ(-Math.PI / 2);
scene.add(mesh);
} else if (file.name.endsWith('.drc')) {
// Draco file handling
dracoLoader.decodeDracoFile(event.target.result, function(geometry) {
// Compute normals for Draco geometry if missing
if (!geometry.attributes.normal) {
geometry.computeVertexNormals();
}
const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
vertexColors: true,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.rotateX(-Math.PI / 2);
mesh.rotateZ(-Math.PI / 2);
scene.add(mesh);
});
}
loadedCount++;
if(loadedCount === totalFiles) {
document.getElementById('loading').style.display = 'none';
positionCamera();
isRotating = true;
document.getElementById('rotate-toggle').textContent = 'Pause Rotation';
animate(); // Start animation after loading
}
} catch (error) {
console.error('Error loading PLY file:', error);
loadedCount++;
if(loadedCount === totalFiles) {
document.getElementById('loading').style.display = 'none';
if (scene.children.filter(c => c instanceof THREE.Mesh).length > 0) {
positionCamera();
isRotating = true;
document.getElementById('rotate-toggle').textContent = 'Pause Rotation';
animate(); // Start animation after loading
}
}
}
};
reader.readAsArrayBuffer(file);
});
});
// Position camera reset
function positionCamera() {
// Initial camera position
scene.rotation.y = 0;
camera.position.set(0, 0, 0);
camera.lookAt(0, 0, -10);
}
// Control button events
document.getElementById('rotate-toggle').addEventListener('click', function() {
isRotating = !isRotating;
this.textContent = isRotating ? 'Pause Rotation' : 'Start Rotation';
});
document.getElementById('reset-view').addEventListener('click', function() {
positionCamera();
isAtLimit = false;
isRotating = true;
if (!animationId) {
animate();
}
});
// Function to limit movement distance
function limitMovement(position) {
const distance = Math.sqrt(position.x * position.x + position.z * position.z);
if (distance > maxDistance) {
const ratio = maxDistance / distance;
position.x *= ratio;
position.z *= ratio;
return true; // Reached limit
}
return false; // Not at limit
}
// Animation loop
function animate() {
// Calculate movement direction based on camera orientation
if (keys.w || keys.a || keys.s || keys.d) {
// Get camera's forward vector (negative Z-axis)
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
// Get camera's right vector (positive X-axis)
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
// Ignore Y component to keep movement horizontal
forward.y = 0;
right.y = 0;
forward.normalize();
right.normalize();
// Calculate movement direction
const movement = new THREE.Vector3();
if (keys.w) movement.add(forward); // Forward
if (keys.s) movement.sub(forward); // Backward
if (keys.a) movement.sub(right); // Left
if (keys.d) movement.add(right); // Right
// Only normalize if there's actual movement
if (movement.length() > 0) {
movement.normalize().multiplyScalar(moveSpeed);
}
// Store current Y position
const currentY = camera.position.y;
// Apply movement
camera.position.add(movement);
// Restore Y position to keep movement horizontal
camera.position.y = currentY;
// Check if reached movement limit
isAtLimit = limitMovement(camera.position);
}
// Auto-rotation if enabled
if (isRotating && scene.children.some(c => c instanceof THREE.Mesh)) {
scene.rotation.y += rotationSpeed;
}
// Create a target position slightly in front of the camera
const targetPosition = new THREE.Vector3();
targetPosition.copy(camera.position);
targetPosition.add(new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion));
// Smoothly look at a point slightly in front of the camera
camera.lookAt(targetPosition);
renderer.render(scene, camera);
// keep running
animationId = requestAnimationFrame(animate);
}
// Initial animation start
animate();
// Window resize handler
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>