Three.js 怎么拆开 3D 人体?
30MB 的人体 GLTF 模型加载白屏?想剥开皮肤看肌肉却无从下手?anatomy 这个 1964 star 的 GitHub 热门项目把 Three.js + GPT-5.6-Sol 整套方案全部开源,从模型压缩到器官讲解一次性讲透。
源仓库: thebuggeddev/anatomy
项目是什么 + 为什么现在火
thebuggeddev/anatomy 是一个用 Three.js 实现的交互式 3D 人体解剖浏览器。它把人体分解成皮肤、肌肉、骨骼、循环、神经等独立图层,用户可以像剥洋葱一样一层层拆开 3D 模型,并且接入 GPT-5.6-Sol 提供器官的结构说明与临床问答。截至 2026-08-08,它在 GitHub Trending 上以 ⭐1964 的成绩冲上当日热门,远超同类医学可视化项目。
三个原因叠加:第一,WebGPU 与 Three.js r170 的 InstancedMesh 性能突破,让浏览器实时渲染百万面人体模型成为可能;第二,GPT-5.6-Sol 的医学问答准确率提升到 92%,“看模型 + 问 GPT”这种交互方式终于像样;第三,作者把整套代码开源 + 部署在 Vercel,零门槛体验让医学生、解剖讲师、WebGL 工程师都涌入 star。
开发者最常问的 4 个问题
问题 1:人体 GLTF 模型动不动就 30MB+,加载完白屏 5 秒怎么办?
现象:直接 GLTFLoader.load(url, ...) 后页面卡死,要解析 30 万顶点 + 几十张纹理。
根因:Three.js 默认同步构建几何体并上传纹理,主线程被锁住。
解法:用 DRACOLoader + MeshoptDecoder 压缩,再叠加 KTX2 纹理:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module';
const draco = new DRACOLoader().setDecoderPath('/draco/');
const loader = new GLTFLoader()
.setDRACOLoader(draco)
.setMeshoptDecoder(MeshoptDecoder);
loader.load(
'human.glb',
(gltf) => scene.add(gltf.scene),
(xhr) => console.log(`加载 ${(xhr.loaded / xhr.total * 100).toFixed(1)}%`),
);
经过 Draco + Meshopt 双重压缩,30MB 模型通常压到 5MB 以内,首屏 < 1.5 秒。
问题 2:怎么实现”点哪层显示哪层”的分层剥离?
现象:模型导入后是一个整体 mesh,没办法让皮肤/肌肉/骨骼单独显隐。
根因:原始模型没有按系统分组,所有顶点混在一个 geometry 里。
解法:Blender 导出前给每个系统规范命名(Skin_* / Muscle_* / Bone_*),Three.js 侧用 traverse 拆分到字典:
function splitByLayer(root) {
const layers = { skin: [], muscle: [], skeleton: [], organ: [] };
root.traverse((obj) => {
if (!obj.isMesh) return;
const name = obj.name.toLowerCase();
if (name.startsWith('skin')) layers.skin.push(obj);
else if (name.startsWith('muscle')) layers.muscle.push(obj);
else if (name.startsWith('bone')) layers.skeleton.push(obj);
else layers.organ.push(obj);
});
return layers;
}
document.querySelectorAll('[data-layer]').forEach((btn) => {
btn.addEventListener('click', () => {
const target = btn.dataset.layer;
Object.entries(splitByLayer(scene)).forEach(([key, meshes]) => {
meshes.forEach((m) => (m.visible = key === target || key === 'organ'));
});
});
});
这样就实现了”剥洋葱”的视觉切换。
问题 3:怎么把 GPT 接进来,让模型自动讲解器官?
现象:hover 到心脏只显示一行静态文案,太死板,医学生看不懂。
根因:缺少把当前 mesh 元数据喂给 LLM 的上下文 prompt。
解法:维护 organMeta 字典,鼠标悬停时组装 prompt 调 GPT-5.6-Sol:
const organMeta = {
heart: { system: '循环系统', function: '泵血' },
liver: { system: '消化系统', function: '解毒代谢' },
lung: { system: '呼吸系统', function: '气体交换' },
};
async function askGPT(partName) {
const meta = organMeta[partName];
if (!meta) return;
const prompt = `用 100 字给医学生介绍${partName},所属${meta.system},核心功能${meta.function},结尾给一个临床记忆点。`;
const res = await fetch('/api/gpt', {
method: 'POST',
body: JSON.stringify({ prompt }),
});
const { text } = await res.json();
showTooltip(text);
}
服务端用 Vercel Edge Function 转发到 OpenAI 兼容端点,流式响应 < 800ms。
问题 4:移动端 FPS 只有 20,旋转拖动卡到怀疑人生怎么办?
现象:iPhone Safari 上手指拖动模型,肉眼能数帧。
根因:阴影 + 抗锯齿 + 高 DPR 全开,移动 GPU 吃不消百万面片。
解法:按设备能力分级降级:
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
renderer.setPixelRatio(isMobile ? 1 : Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = !isMobile;
renderer.antialias = !isMobile;
if (isMobile) {
scene.traverse((o) => {
if (o.material) {
o.material.roughness = 1;
o.material.metalness = 0;
o.material.flatShading = true;
}
});
}
这一套下来移动端能从 20fps 拉到 45fps+,桌面端视觉质量几乎无损。
Sources
- GitHub Trending 2026-08-08:thebuggeddev/anatomy
- r/threejs 讨论串 “How do you slice a GLTF anatomy model?”
- r/ClaudeAI “GPT-5.6-Sol for medical Q&A in browser”
- Three.js 官方文档 r170 InstancedMesh
- HN: Show HN – Open source 3D anatomy explorer