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