295 lines
10 KiB
HTML
295 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>34 - XYZRGB Dragon (Color Scan)</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
overflow: hidden;
|
|
background: #050505;
|
|
font-family: 'Segoe UI', monospace;
|
|
}
|
|
#canvas-container {
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
}
|
|
#ui {
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
pointer-events: none;
|
|
z-index: 10;
|
|
}
|
|
#title {
|
|
position: absolute;
|
|
top: 20px; left: 25px;
|
|
color: #fff;
|
|
font-size: 18px;
|
|
}
|
|
#title span { color: #00ff88; }
|
|
#stats {
|
|
position: absolute;
|
|
top: 20px; right: 25px;
|
|
color: #888;
|
|
text-align: right;
|
|
font-size: 11px;
|
|
}
|
|
#stats .big { font-size: 18px; color: #fff; }
|
|
#loading-overlay {
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
background: rgba(0,0,0,0.95);
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 100;
|
|
}
|
|
#loading-bar-container {
|
|
width: 350px;
|
|
height: 15px;
|
|
background: #333;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
margin: 20px 0;
|
|
}
|
|
#loading-bar {
|
|
width: 0%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #00ff88, #00d4ff);
|
|
transition: width 0.3s;
|
|
}
|
|
#controls {
|
|
position: absolute;
|
|
bottom: 25px; left: 25px;
|
|
pointer-events: auto;
|
|
background: rgba(0,0,0,0.85);
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
}
|
|
#controls button {
|
|
background: rgba(255,255,255,0.08);
|
|
border: 1px solid #555;
|
|
color: #aaa;
|
|
padding: 6px 12px;
|
|
margin: 3px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 10px;
|
|
}
|
|
#controls button:hover, #controls button.active {
|
|
background: rgba(0,255,136,0.3);
|
|
border-color: #00ff88;
|
|
color: #fff;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="loading-overlay">
|
|
<div style="color:#00ff88;font-size:24px;">🐉 XYZRGB Dragon</div>
|
|
<div style="color:#666;font-size:12px;margin-top:10px;">彩色扫描数据 | 68MB</div>
|
|
<div id="loading-bar-container"><div id="loading-bar"></div></div>
|
|
<div id="loading-status" style="color:#888;font-size:12px;">准备加载...</div>
|
|
</div>
|
|
|
|
<div id="canvas-container"></div>
|
|
|
|
<div id="ui">
|
|
<div id="title">34 - <span>XYZRGB Dragon</span> 🐉</div>
|
|
<div id="stats">
|
|
<div class="big">3.6M 顶点</div>
|
|
<div>带原始颜色</div>
|
|
<div>68MB 压缩</div>
|
|
</div>
|
|
|
|
<div id="controls">
|
|
<div style="color:#00ff88;font-size:11px;margin-bottom:5px;">采样率</div>
|
|
<button onclick="setDecimation(1)" id="btn-d1">1:1</button>
|
|
<button onclick="setDecimation(10)" class="active" id="btn-d10">1:10</button>
|
|
<button onclick="setDecimation(50)" id="btn-d50">1:50</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js"></script>
|
|
|
|
<script>
|
|
// ========== 场景 ==========
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x050505);
|
|
|
|
const camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.001, 100);
|
|
camera.position.set(0.5, 0.3, 0.8);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
|
|
|
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.autoRotate = true;
|
|
controls.autoRotateSpeed = 0.5;
|
|
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
|
|
|
let currentMesh = null;
|
|
let modelData = null;
|
|
let decimation = 10;
|
|
|
|
// ========== 加载 ==========
|
|
async function loadDragon() {
|
|
const status = document.getElementById('loading-status');
|
|
const bar = document.getElementById('loading-bar');
|
|
|
|
status.textContent = '下载 68MB 压缩文件...';
|
|
const response = await fetch('../../stanfordRecon/xyzrgb_dragon.ply.gz');
|
|
const reader = response.body.getReader();
|
|
const contentLength = +response.headers.get('Content-Length') || 68*1024*1024;
|
|
|
|
const chunks = [];
|
|
let received = 0;
|
|
|
|
while (true) {
|
|
const {done, value} = await reader.read();
|
|
if (done) break;
|
|
chunks.push(value);
|
|
received += value.length;
|
|
const pct = (received/contentLength*50);
|
|
bar.style.width = pct + '%';
|
|
status.textContent = `下载中... ${(received/1024/1024).toFixed(1)}MB`;
|
|
}
|
|
|
|
status.textContent = '解压中...';
|
|
bar.style.width = '60%';
|
|
|
|
// 合并并解压
|
|
const allChunks = new Uint8Array(received);
|
|
let pos = 0;
|
|
for (const chunk of chunks) {
|
|
allChunks.set(chunk, pos);
|
|
pos += chunk.length;
|
|
}
|
|
|
|
const decompressed = pako.inflate(allChunks);
|
|
bar.style.width = '80%';
|
|
status.textContent = '解析 PLY...';
|
|
|
|
// 解析 PLY
|
|
const data = parsePLY(decompressed.buffer);
|
|
modelData = data;
|
|
|
|
bar.style.width = '100%';
|
|
status.textContent = '完成!';
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('loading-overlay').style.display = 'none';
|
|
createMesh(data, decimation);
|
|
}, 300);
|
|
}
|
|
|
|
function parsePLY(buffer) {
|
|
const decoder = new TextDecoder();
|
|
const data = new Uint8Array(buffer);
|
|
|
|
let headerEnd = 0;
|
|
for (let i = 0; i < Math.min(data.length, 10000); i++) {
|
|
const slice = decoder.decode(data.slice(i, i+20));
|
|
if (slice.includes('end_header')) {
|
|
headerEnd = i + 13;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const header = decoder.decode(data.slice(0, headerEnd));
|
|
const numVertices = parseInt(header.match(/element vertex (\d+)/)[1]);
|
|
|
|
const vertices = new Float32Array(numVertices * 3);
|
|
const colors = new Float32Array(numVertices * 3);
|
|
|
|
const view = new DataView(buffer, headerEnd);
|
|
let offset = 0;
|
|
|
|
for (let i = 0; i < numVertices; i++) {
|
|
vertices[i*3] = view.getFloat32(offset, true);
|
|
vertices[i*3+1] = view.getFloat32(offset+4, true);
|
|
vertices[i*3+2] = view.getFloat32(offset+8, true);
|
|
colors[i*3] = view.getUint8(offset+12) / 255;
|
|
colors[i*3+1] = view.getUint8(offset+13) / 255;
|
|
colors[i*3+2] = view.getUint8(offset+14) / 255;
|
|
offset += 15; // x,y,z + r,g,b
|
|
}
|
|
|
|
return { vertices, colors, numVertices };
|
|
}
|
|
|
|
function createMesh(data, decim) {
|
|
if (currentMesh) {
|
|
scene.remove(currentMesh);
|
|
currentMesh.geometry.dispose();
|
|
}
|
|
|
|
const count = Math.floor(data.numVertices / decim);
|
|
const decimatedVerts = new Float32Array(count * 3);
|
|
const decimatedColors = new Float32Array(count * 3);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const idx = i * decim * 3;
|
|
decimatedVerts[i*3] = data.vertices[idx];
|
|
decimatedVerts[i*3+1] = data.vertices[idx+1];
|
|
decimatedVerts[i*3+2] = data.vertices[idx+2];
|
|
decimatedColors[i*3] = data.colors[idx];
|
|
decimatedColors[i*3+1] = data.colors[idx+1];
|
|
decimatedColors[i*3+2] = data.colors[idx+2];
|
|
}
|
|
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(decimatedVerts, 3));
|
|
geometry.setAttribute('color', new THREE.BufferAttribute(decimatedColors, 3));
|
|
|
|
const material = new THREE.PointsMaterial({
|
|
size: decim > 20 ? 0.02 : 0.005,
|
|
vertexColors: true,
|
|
sizeAttenuation: true
|
|
});
|
|
|
|
currentMesh = new THREE.Points(geometry, material);
|
|
currentMesh.scale.set(0.002, 0.002, 0.002);
|
|
|
|
geometry.computeBoundingBox();
|
|
const center = new THREE.Vector3();
|
|
geometry.boundingBox.getCenter(center);
|
|
geometry.translate(-center.x, -center.y, -center.z);
|
|
|
|
scene.add(currentMesh);
|
|
}
|
|
|
|
function setDecimation(d) {
|
|
decimation = d;
|
|
document.querySelectorAll('#controls button').forEach(btn => btn.classList.remove('active'));
|
|
document.getElementById('btn-d' + d).classList.add('active');
|
|
if (modelData) createMesh(modelData, d);
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
loadDragon();
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html>
|