Spaces:
Build error
Build error
<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> |