211 lines
9.1 KiB
HTML
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>
|