前端实现交互式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

Flutter Web 开发:解决跨域(CORS)问题的终极指南

Flutter Web 开发:解决跨域(CORS)问题的终极指南

Flutter Web 开发:解决跨域(CORS)问题的终极指南 在 Flutter Web 开发过程中,默认情况下浏览器会遵循同源策略。当你的应用尝试加载不同域名的网络资源(如 API 接口、图片等)时,经常会遇到 CORS(跨域资源共享) 错误,导致请求失败。 虽然生产环境应由后端配置 CORS 头来解决,但在本地开发和调试阶段,我们可以通过修改 Flutter 工具链源码来临时禁用浏览器的安全策略,从而顺利调试。 以下是详细的操作步骤: 🛠️ 操作步骤 第一步:定位 chrome.dart 文件 首先,你需要找到 Flutter SDK 中负责启动 Chrome 浏览器的配置文件 chrome.dart。 参考路径(请根据你的实际安装路径调整): <你的

下载安装Microsoft Edge Webview2教程

下载安装Microsoft Edge Webview2教程

视频教程 Windows 10/11系统 Webview2安装——win10/11 Windows 7系统 Webview2安装——Win7 图文教程 官网下载最新版Webview2安装包 点击下载安装 官网地址:Microsoft Edge WebView2 | Microsoft Edge Developer 1. 进入官网,点击下载按钮 2. 点击左侧常青引导程序下载按钮 3. 在弹出的页面点击接受并下载,右上角下载管理页面在下载完成后有文件弹出 4. 在游览器下载管理页面直接点击打开文件进行软件的安装 5. 软件安装中,安装完成后无需手动点击自动弹出消失。 graph TD A[安装码尚云标签] --> B{判断安装情况} B -->|Yes| C[打开软件进行标签设计] B --&

前端文件上传方案:别再只用input type=file了

前端文件上传方案:别再只用input type=file了

前端文件上传方案:别再只用input type=file了 毒舌时刻 这代码写得跟网红滤镜似的——仅供参考。 各位前端同行,咱们今天聊聊前端文件上传。别告诉我你还在用原生的input上传大文件,那感觉就像在用小水管灌满游泳池——慢得让人绝望。 为什么你需要文件上传方案 最近看到一个项目,上传100MB的文件直接卡死浏览器,没有任何进度提示,我差点当场去世。我就想问:你是在做上传还是在做浏览器杀手? 反面教材 <!-- 反面教材:原生文件上传 --> <input type="file" onchange="uploadFile(this.files[0])" /> <script> function uploadFile(file) { const formData = new FormData(

微信 H5 缓存控制:后端重定向 & 前端强制刷新

在 Web 开发中,缓存是一把双刃剑。对于静态资源,它能极大提升加载速度;但对于业务逻辑频繁变动的 H5 页面(如支付、订单页),缓存往往会导致用户看到过期的数据或界面。最近在维护一个 uni-app 项目时,遇到了一段关于 H5 缓存控制的逻辑,引发了我对于“后端重定向加时间戳”和“前端 JS 加时间戳”这两种方案的思考。虽然两者的最终目的一致,但在 Hash 模式下,它们的实现原理和效果有着本质的区别。 一、 问题背景 在应用启动的生命周期中,通常会有这样一段逻辑:当用户访问特定的关键页面(如支付、订单页)时,如果当前 URL 中缺少时间戳参数,前端会自动解析 URL,追加当前时间戳,并强制页面刷新。 这就引出了一个问题:为什么不直接在后端重定向时加时间戳?这两种方式有什么区别? 二、 核心区别: