前端实现交互式3D人体肌肉解剖图:基于 Three.js + React Three Fiber 的完整方案


本文将详细介绍如何在前端实现一个交互式的3D人体肌肉解剖展示工具,用户可以旋转、缩放模型,点击任意肌肉查看中英文名称。

为什么要做这个?

传统的肌肉解剖学习通常依赖静态图片或昂贵的3D软件。作为健身爱好者,我希望能有一个免费、易用的在线工具来学习肌肉解剖知识。于是我决定自己动手,基于开源的 Z-Anatomy 项目,在浏览器中实现一个交互式的3D肌肉解剖图。

如果你想先体验效果,可以试试这个在线的3D肌肉功能解剖工具。

技术架构概览

┌─────────────────────────────────────────────────────────────┐ │ 用户浏览器 │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ GLB 模型 │ -> │ Three.js │ -> │ React Three │ │ │ │ (Draco) │ │ 场景渲染 │ │ Fiber 组件 │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ │ │ │ ┌──────────────────┼──────────────────┐ │ │ v v v │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 射线检测 │ │ 肌肉高亮 │ │ i18n 翻译 │ │ │ │ Raycaster │ │ 材质切换 │ │ 中英文名称 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ 

模型来源与处理

Z-Anatomy 开源项目

Z-Anatomy 是一个开源的人体解剖学 Blender 项目,包含完整的人体肌肉、骨骼、器官等结构。原始文件约 300MB,包含 1000+ 个独立的 mesh 对象。

模型导出与压缩

为了在 Web 端使用,我们需要将 Blender 文件导出为 GLB 格式,并使用 Draco 压缩:

# export_cli.py - Blender 命令行导出脚本import bpy # 选择所有肌肉相关的对象 muscle_keywords =['muscle','deltoid','bicep','tricep','pectoralis',...]for obj in bpy.data.objects:if obj.type=='MESH': name_lower = obj.name.lower()ifany(keyword in name_lower for keyword in muscle_keywords): obj.select_set(True)# 导出为 GLB,启用 Draco 压缩 bpy.ops.export_scene.gltf( filepath='muscle-anatomy.glb', export_format='GLB', use_selection=True, export_draco_mesh_compression_enable=True, export_draco_mesh_compression_level=6,)

导出后的模型从 300MB 压缩到约 6.8MB,非常适合 Web 加载。

核心实现

1. 场景搭建 (React Three Fiber)

// muscle-scene.tsx import { Canvas } from '@react-three/fiber'; import { OrbitControls, Environment } from '@react-three/drei'; export function MuscleScene({ onMuscleClick, onMuscleHover, hoveredMuscle, selectedMuscle }) { return ( <Canvas camera={{ position: [1.5, 0.3, 2], fov: 50 }}> {/* 环境光照 */} <ambientLight intensity={0.6} /> <directionalLight position={[5, 5, 5]} intensity={0.8} /> {/* 3D 模型 */} <Suspense fallback={null}> <MuscleModel onMuscleClick={onMuscleClick} onMuscleHover={onMuscleHover} hoveredMuscle={hoveredMuscle} selectedMuscle={selectedMuscle} /> </Suspense> {/* 轨道控制器 */} <OrbitControls enablePan={false} minDistance={1} maxDistance={5} target={[0, 0.5, 0]} /> {/* 环境贴图 */} <Environment preset="studio" /> </Canvas> ); } 

2. 模型加载与肌肉识别

// muscle-model.tsx import { useGLTF } from '@react-three/drei'; import * as THREE from 'three'; // 肌肉识别关键词 const muscleKeywords = [ 'muscle', 'deltoid', 'bicep', 'tricep', 'pectoralis', 'latissimus', 'trapezius', 'rectus', 'oblique', 'gluteus', 'quadricep', 'vastus', 'hamstring', 'gastrocnemius', 'soleus', // ... 更多关键词 ]; // 需要隐藏的非肌肉对象 const HIDDEN_KEYWORDS = [ 'region', 'fascia', 'bursa', 'ligament', 'membrane', '.j', '.t', // Z-Anatomy 的标签后缀 ]; function isMuscle(name: string): boolean { const lowerName = name.toLowerCase(); return muscleKeywords.some(keyword => lowerName.includes(keyword)) && !HIDDEN_KEYWORDS.some(keyword => lowerName.includes(keyword)); } export function MuscleModel({ onMuscleClick, onMuscleHover, hoveredMuscle, selectedMuscle }) { const { scene } = useGLTF('/models/muscle-anatomy.glb'); // 克隆场景并处理材质 const clonedScene = useMemo(() => { const clone = scene.clone(true); clone.traverse((child) => { if (child instanceof THREE.Mesh) { if (shouldHide(child.name)) { child.parent?.remove(child); return; } if (isMuscle(child.name)) { // 为肌肉设置红色系材质 child.material = child.material.clone(); child.material.color = new THREE.Color(0xc44d4d); child.material.roughness = 0.7; child.material.metalness = 0.1; } } }); return clone; }, [scene]); return ( <primitive object={clonedScene} position={[0, -0.84, 0]} onPointerOver={handlePointerOver} onPointerOut={handlePointerOut} onClick={handleClick} /> ); } 

3. 交互高亮效果

// 高亮颜色配置 const HIGHLIGHT_COLOR = new THREE.Color(0x5ac57a); // 薄荷绿 - 悬停 const SELECTED_COLOR = new THREE.Color(0x4caf50); // 深绿 - 选中 // 更新高亮效果 useEffect(() => { clonedScene.traverse((child) => { if (child instanceof THREE.Mesh && isMuscle(child.name)) { const material = child.material as THREE.MeshStandardMaterial; const muscleId = getMuscleIdFromModelName(child.name); const isHovered = muscleId === hoveredMuscle; const isSelected = muscleId === selectedMuscle; if (isSelected) { material.emissive = SELECTED_COLOR; material.emissiveIntensity = 0.5; } else if (isHovered) { material.emissive = HIGHLIGHT_COLOR; material.emissiveIntensity = 0.3; } else { material.emissive = new THREE.Color(0x000000); material.emissiveIntensity = 0; } } }); }, [hoveredMuscle, selectedMuscle, clonedScene]); 

4. 肌肉名称映射与多语言

Z-Anatomy 的命名格式是 Muscle name.l(左侧)/ Muscle name.r(右侧),我们需要将其映射到标准的肌肉 ID:

// muscles.tsconst muscleAliases: Record<string,string[]>={'pectoralis_major':['pectoralis major','sternocostal head of pectoralis major','clavicular head of pectoralis major',],'biceps_brachii':['biceps brachii','long head of biceps brachii','short head of biceps brachii',],// ... 更多映射};exportfunctiongetMuscleIdFromModelName(modelName:string):string|undefined{const cleanName = modelName .replace(/\.(l|r|el|er)$/i,'')// 移除左右后缀.toLowerCase().trim();for(const[muscleId, aliases]of Object.entries(muscleAliases)){if(aliases.some(alias => cleanName.includes(alias))){return muscleId;}}returnundefined;}

多语言翻译存储在 i18n 模块中:

// locales/zh/muscle-anatomy.tsexportconst muscleAnatomy ={ muscles:{ pectoralis_major:'胸大肌', biceps_brachii:'肱二头肌', triceps_brachii:'肱三头肌', latissimus_dorsi:'背阔肌', rectus_abdominis:'腹直肌', gluteus_maximus:'臀大肌', quadriceps_femoris:'股四头肌', gastrocnemius:'腓肠肌',// ... 150+ 肌肉翻译},};

性能优化

1. 模型压缩

  • 使用 Draco 压缩,模型从 300MB 压缩到 6.8MB
  • 在 Blender 中使用 Decimate Modifier 降低面数

2. 动态加载

// 使用 Next.js 动态导入,禁用 SSR const MuscleScene = dynamic( () => import('./muscle-scene').then((mod) => mod.MuscleScene), { ssr: false } ); 

3. 材质优化

  • 克隆材质避免全局修改
  • 使用 emissive 属性实现高亮,避免重新创建材质

应用场景

这套技术方案可以应用于多种场景:

  1. 健身教育:帮助健身爱好者了解肌肉位置和名称
  2. 医学教学:辅助医学生学习人体解剖
  3. 康复指导:展示特定肌肉群的位置
  4. 游戏开发:角色肌肉系统参考

如果你对健身相关的工具感兴趣,可以看看这些在线计算器:

总结

本文介绍了如何在前端使用 Three.js + React Three Fiber 实现交互式3D人体肌肉解剖展示。核心技术点包括:

  1. 模型处理:从 Z-Anatomy 导出并压缩 GLB 模型
  2. 场景搭建:使用 React Three Fiber 构建 3D 场景
  3. 交互实现:射线检测 + 材质高亮
  4. 多语言支持:肌肉名称中英文翻译

这套方案完全在浏览器端运行,无需后端服务,非常适合构建教育类的3D可视化应用。


相关链接:

Read more

Chrome 开发者工具(DevTools)快速入门——前端必备技能

Chrome 开发者工具(DevTools)快速入门——前端必备技能

在前端开发中,Chrome 开发者工具(DevTools) 是我们排查问题、优化性能、理解浏览器行为最强大的助手之一。无论是想快速定位页面样式异常、分析接口请求,还是调试 JavaScript 逻辑,DevTools 都能提供直观高效的方式帮助我们“看清代码背后发生了什么”。 这篇笔记将带你快速入门,从最基本的面板功能到常用调试技巧,让你能在实战中灵活运用它,提升开发效率与问题解决能力。 1. 打开方式与放大缩小 1. 快捷键: * Windows/Linux:F12 或 Ctrl + Shift + I * macOS:Command + Option + I 1. 右键打开:在页面任意元素上右键 → “检查 (Inspect)” 2. 菜单打开:点击右上角 ⋮ → “更多工具” → “开发者工具” 在使用 Chrome 开发者工具时,可以通过 鼠标滚轮 快速调整界面缩放。

Flutter-WebRTC 鸿蒙平台使用指南

1. 插件介绍 fluttertpc_flutter_webrtc 是一个专为开源鸿蒙(HarmonyOS)平台适配的 Flutter WebRTC 插件,基于官方的 flutter_webrtc 项目进行定制修改,提供了完整的 WebRTC 功能支持,包括实时音视频通信、设备管理、屏幕共享等核心能力。 主要功能特性 * 设备管理:支持枚举、选择音频/视频输入输出设备 * 媒体流处理:获取摄像头、麦克风媒体流,支持自定义分辨率和帧率 * 对等连接:实现完整的 WebRTC 信令流程和媒体协商 * 屏幕共享:支持获取屏幕共享媒体流 * 数据通道:支持点对点数据传输 2. 插件安装与配置 2.1 Git 依赖引入 由于这是一个专为鸿蒙平台定制的修改版本,需要通过 Git 方式引入依赖。在 Flutter

【前端地图】地图基本操作控制——平移、缩放、旋转、手势控制、地图样式切换、全屏模式支持

【前端地图】地图基本操作控制——平移、缩放、旋转、手势控制、地图样式切换、全屏模式支持

🎮 第3节 | 地图基本操作控制 🎯 学习目标 老曹说:“地图不能动算什么地图?今天教你让地图飞起来!” 1. 🧭 掌握地图平移、缩放、旋转三大基础操作 2. 🖱️ 理解手势控制与事件监听机制 3. 🎨 实现地图样式动态切换(白天/黑夜/卫星图) 4. 🖥️ 支持全屏模式与自适应布局 🧠 引言:让用户掌控地图! 地图不只是看的,更是用来“玩”的!优秀的用户体验离不开流畅的操作反馈。本节课带你解锁地图操控的所有姿势,让你的地图像游戏一样丝滑。 老曹吐槽时间: “有些产品地图做得跟 PPT 似的,点都点不动……用户:你礼貌吗?” 🧭 一、三大基础操作详解 1.1 平移(Pan)📍 平移是最常见的操作,通过改变地图中心点实现视角移动。 ✅ 高德地图实现 // 方法一:直接设置中心点 map.setCenter([116.404,

前端人拿不到offer,九成是不知道这个新风向

今年大部分互联网公司面试的题目已经开始小部分八股文,大部分场景题了,公司需要的不仅是知识扎实,而且招进来就能上手项目的面试者… 2026最新高频场景题 * 1. 请求失败会弹出一个toast,如何保证批量请求失败,只弹出一个toast * 2. 如何减少项目里面if-else * 3. babel-runtime 作用是啥 * 4. 如何实现预览PDF文件 * 5. 如何在划词选择的文本上添加右键菜单(划词:鼠标滑动选择一组字符,对组字符进行操作) * 6. 富文本里面,是如何做到划词的(鼠标滑动选择一组字符,对组字符进行操作)? * 7. 如何做好前端监控方案 * 8. 如何标准化处理线上用户反馈的问题 * 9. px如何转为rem * 10. 浏览器有同源策略,但是为何 cdn 请求资源的时候不会有 跨域限制 * 11. cookie可以实现不同域共享吗 * 12. axios是否可以取消请求 * 13. 前端如何实现折叠面板效果? * 14. dom里面,如何判定a元素是否是b元素的子元 * 15. 判断一个对象是否为空,包含了其原型链上是否有自