Files
nanocode/particle-lab/34-xyzrgb-dragon/index.html
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

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>