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

211 lines
9.1 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>35 - XYZRGB Manuscript</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: #0a0a0a;
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: #d4a574; }
#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, #d4a574, #8b7355); 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(212,165,116,0.3); border-color: #d4a574; color: #fff; }
</style>
</head>
<body>
<div id="loading-overlay">
<div style="color:#d4a574;font-size:24px;">📜 XYZRGB Manuscript</div>
<div style="color:#666;font-size:12px;margin-top:10px;">古代手稿扫描 | 42MB</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">35 - <span>XYZRGB Manuscript</span> 📜</div>
<div id="stats">
<div class="big">2.3M 顶点</div>
<div>原始颜色保留</div>
<div>42MB 压缩</div>
</div>
<div id="controls">
<div style="color:#d4a574;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(0x0a0a0a);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.001, 100);
camera.position.set(0.3, 0.2, 0.5);
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;
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
let currentMesh = null, modelData = null, decimation = 10;
async function loadModel() {
const status = document.getElementById('loading-status');
const bar = document.getElementById('loading-bar');
status.textContent = '下载 42MB 压缩文件...';
const response = await fetch('../../stanfordRecon/xyzrgb_manuscript.ply.gz');
const reader = response.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const {done, value} = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
bar.style.width = Math.min(received/(42*1024*1024)*50, 50) + '%';
}
status.textContent = '解压中...';
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...';
modelData = parsePLY(decompressed.buffer);
bar.style.width = '100%';
setTimeout(() => {
document.getElementById('loading-overlay').style.display = 'none';
createMesh(modelData, 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++) {
if (decoder.decode(data.slice(i, i+20)).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);
for (let i = 0, offset = 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;
}
return { vertices, colors, numVertices };
}
function createMesh(data, decim) {
if (currentMesh) { scene.remove(currentMesh); currentMesh.geometry.dispose(); }
const count = Math.floor(data.numVertices / decim);
const verts = new Float32Array(count * 3);
const cols = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const idx = i * decim * 3;
verts[i*3] = data.vertices[idx];
verts[i*3+1] = data.vertices[idx+1];
verts[i*3+2] = data.vertices[idx+2];
cols[i*3] = data.colors[idx];
cols[i*3+1] = data.colors[idx+1];
cols[i*3+2] = data.colors[idx+2];
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(verts, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(cols, 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.003, 0.003, 0.003);
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);
});
loadModel();
animate();
</script>
</body>
</html>