Skip to content

Commit 0aebaa6

Browse files
authored
更改了README.md,使设计的描述更加详细 (#3057)
* 完成选题并提交 * PyCharm 新建 Flask 项目(虚拟环境隔离依赖) * 具身人 3D 模型获取与本地可视化测试 * 更改了README.md,描述更加详细 * 创建机器人模型 * 创建机器人模型
1 parent 6b1b92d commit 0aebaa6

File tree

7 files changed

+319
-20
lines changed

7 files changed

+319
-20
lines changed

src/embodied_human/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/embodied_human/humanoid_flask_3d/ templates/index.html

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/embodied_human/humanoid_flask_3d/app.py

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# 具身人3D可视化与动作交互网页开发
2+
基于Python+Flask+PyVista的3D可视化系统,支持模型视角控制、颜色切换、基础动作交互,通过GitHub Pages实现公开访问。
3+
4+
## 项目概述
5+
本项目以Flask为Web框架,结合PyVista/Trimesh实现具身人3D模型的Web化渲染,完成基础交互(视角旋转/缩放、颜色切换)与进阶动作控制(抬手、抬腿等预设动作),并通过GitHub完成代码管理与静态网页部署,最终实现可公开访问的具身人3D交互网页。
6+
7+
## 环境准备
8+
### 开发工具
9+
- PyCharm(社区版):项目管理、编码与调试
10+
- GitHub:代码托管与Pages部署
11+
- Chrome/Firefox:Web功能测试
12+
13+
### 依赖库安装
14+
```bash
15+
# 虚拟环境下执行
16+
pip install flask pyvista trimesh numpy
17+
```
18+
| 依赖库 | 功能说明 |
19+
|-------------|------------------------------|
20+
| flask | Web框架,实现路由与页面渲染 |
21+
| pyvista | 3D可视化核心,支持模型Web适配 |
22+
| trimesh | 模型关节控制,处理带骨骼模型 |
23+
| numpy | 数值计算,实现关节角度转换 |
24+
25+
### 模型准备
26+
下载带骨骼绑定的具身人3D模型(GLB/FBX格式),推荐来源:
27+
- Sketchfab官网搜索「Free Rigged Humanoid GLB」
28+
- 筛选条件:Downloadable + Low Poly(<50MB)
29+
- 模型放置路径:项目根目录(命名为`humanoid_rigged.glb`
30+
31+
## 项目结构
32+
| 文件名/目录 | 功能描述 |
33+
|--------------------------|--------------------------------------------------------------|
34+
| `app.py` | Flask核心入口,实现首页路由与Web服务启动 |
35+
| `test_model.py` | 本地模型加载与关节控制测试脚本 |
36+
| `generate_web_model.py` | 将本地3D模型转换为Web可加载的JS文件 |
37+
| `generate_animations.py` | 生成预设动作(初始/抬手/抬腿)的Web模型文件 |
38+
| `build_static.py` | 将动态项目转换为静态网页文件,适配GitHub Pages部署 |
39+
| `templates/index.html` | 网页模板,包含UI布局、交互按钮、3D渲染容器 |
40+
| `static/js/3d_render.js` | 3D渲染核心脚本,实现模型加载、颜色切换、动作交互 |
41+
| `static/js/*.js` | 自动生成的模型Web文件(基础模型+动作模型) |
42+
| `static_site/` | 静态网页输出目录,用于GitHub Pages部署 |
43+
| `README.md` | 项目说明文档 |
44+
45+
## 核心功能
46+
### 1. 基础3D可视化(Web端)
47+
- 模型渲染:加载带关节的具身人3D模型,支持Web端流畅显示
48+
- 视角交互:鼠标拖拽旋转模型、滚轮缩放视角
49+
- 颜色切换:一键切换模型颜色(蓝色/绿色/粉色)
50+
51+
### 2. 动作交互控制
52+
- 预设动作:支持初始姿态、抬手、抬腿3种基础动作切换
53+
- 关节控制:基于Trimesh修改模型关节角度,实现精准动作控制
54+
- 动作适配:所有动作模型预生成Web文件,保证前端加载效率
55+
56+
### 3. 工程化部署
57+
- 代码管理:通过GitHub实现版本控制,支持代码同步与回溯
58+
- 静态部署:生成纯静态网页文件,通过GitHub Pages实现公开访问
59+
60+
## 使用方法
61+
### 本地开发与测试
62+
#### 步骤1:环境与模型验证
63+
```bash
64+
# 测试本地模型加载
65+
python test_model.py
66+
```
67+
验证效果:弹出3D窗口显示模型,终端输出关节列表。
68+
69+
#### 步骤2:生成Web模型文件
70+
```bash
71+
# 生成基础模型Web文件
72+
python generate_web_model.py
73+
# 生成动作模型Web文件
74+
python generate_animations.py
75+
```
76+
生成结果:`static/js/`目录下新增`humanoid_base.js``anim_*.js`文件。
77+
78+
#### 步骤3:启动本地Web服务
79+
```bash
80+
python app.py
81+
```
82+
访问地址:`http://127.0.0.1:5000`
83+
验证功能:
84+
- 模型正常渲染,支持视角交互
85+
- 颜色按钮切换模型颜色
86+
- 动作按钮切换初始/抬手/抬腿姿态
87+
88+
### GitHub部署(公开访问)
89+
#### 步骤1:生成静态文件
90+
```bash
91+
python build_static.py
92+
```
93+
生成结果:项目根目录新增`static_site/`,包含完整静态网页文件。
94+
95+
#### 步骤2:GitHub Pages部署
96+
1. 打开项目GitHub仓库 → Settings → Pages
97+
2. 选择「Upload your files」,上传`static_site/`内所有文件
98+
3. 等待5-10分钟,访问仓库Pages链接即可公开访问
99+
100+
## 交互操作指南
101+
| 操作类型 | 操作方式 | 效果说明 |
102+
|----------------|-----------------------------------|------------------------------|
103+
| 视角控制 | 鼠标拖拽模型区域 | 360°旋转模型视角 |
104+
| 缩放控制 | 滚轮滚动模型区域 | 放大/缩小模型显示比例 |
105+
| 颜色切换 | 点击「蓝色/绿色/粉色」按钮 | 实时切换模型主体颜色 |
106+
| 动作切换 | 点击「初始姿态/抬手/抬腿」按钮 | 切换模型至对应预设动作姿态 |
107+
108+
## 常见问题与解决方案
109+
| 问题现象 | 原因分析 | 解决方法 |
110+
|------------------------|-----------------------------------|---------------------------------------------------------------|
111+
| 模型加载失败 | 模型格式错误/路径错误 | 确认模型为GLB/FBX带关节格式,检查代码中模型文件名是否匹配 |
112+
| 动作切换无效果 | 关节名称不匹配 | 运行`test_model.py`查看实际关节名,修改`generate_animations.py`中关节配置 |
113+
| GitHub Pages访问异常 | 文件路径错误/部署未完成 | 检查`static_site/`文件完整性,等待部署完成后清除浏览器缓存重试 |
114+
| Web渲染卡顿 | 模型面数过多 | 更换Low Poly模型,或用Blender简化模型面数 |
115+
116+
117+
## 参考资料
118+
- [Flask官方文档](https://flask.palletsprojects.com/)
119+
- [PyVista Web可视化文档](https://docs.pyvista.org/guide/web/index.html)
120+
- [Trimesh模型操作文档](https://trimsh.org/)
121+
- [GitHub Pages部署指南](https://docs.github.com/zh/pages)
122+
- [Sketchfab免费3D模型资源](https://sketchfab.com/)

src/humanoid_3d_animation/app.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from flask import Flask, render_template
2+
3+
app = Flask(__name__)
4+
5+
@app.route('/')
6+
def index():
7+
return render_template('index.html')
8+
9+
if __name__ == '__main__':
10+
app.run(debug=True)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import pyvista as pv
2+
import numpy as np
3+
import vtk
4+
5+
# 生成更像人形的仿真机器人模型(优化比例+结构)
6+
def create_robot(return_parts=False):
7+
# 1. 躯干(更修长,接近人形)
8+
torso = pv.Cube(center=(0, 0, 0.8), x_length=0.4, y_length=0.3, z_length=1.2)
9+
torso["part_id"] = np.zeros(torso.n_points, dtype=int) # 躯干ID=0
10+
11+
# 2. 头部(新增,更像人形)
12+
head = pv.Cube(center=(0, 0, 1.6), x_length=0.3, y_length=0.3, z_length=0.3)
13+
head["part_id"] = np.full(head.n_points, 5, dtype=int) # 头部ID=5
14+
15+
# 3. 右胳膊(上臂+前臂,比例优化)
16+
right_upper_arm = pv.Cube(center=(0.5, 0, 1.0), x_length=0.2, y_length=0.2, z_length=0.5)
17+
right_upper_arm["part_id"] = np.ones(right_upper_arm.n_points, dtype=int) # 右上臂ID=1
18+
right_forearm = pv.Cube(center=(0.9, 0, 1.0), x_length=0.2, y_length=0.2, z_length=0.5)
19+
right_forearm["part_id"] = np.full(right_forearm.n_points, 2, dtype=int) # 右前臂ID=2
20+
21+
# 4. 左胳膊(新增,对称结构)
22+
left_upper_arm = pv.Cube(center=(-0.5, 0, 1.0), x_length=0.2, y_length=0.2, z_length=0.5)
23+
left_upper_arm["part_id"] = np.full(left_upper_arm.n_points, 6, dtype=int) # 左上臂ID=6
24+
left_forearm = pv.Cube(center=(-0.9, 0, 1.0), x_length=0.2, y_length=0.2, z_length=0.5)
25+
left_forearm["part_id"] = np.full(left_forearm.n_points, 7, dtype=int) # 左前臂ID=7
26+
27+
# 5. 右大腿+小腿(比例优化)
28+
right_thigh = pv.Cube(center=(0.2, 0, -0.2), x_length=0.2, y_length=0.2, z_length=0.6)
29+
right_thigh["part_id"] = np.full(right_thigh.n_points, 8, dtype=int) # 右大腿ID=8
30+
right_calf = pv.Cube(center=(0.2, 0, -0.8), x_length=0.2, y_length=0.2, z_length=0.6)
31+
right_calf["part_id"] = np.full(right_calf.n_points, 9, dtype=int) # 右小腿ID=9
32+
33+
# 6. 左大腿+小腿(比例优化)
34+
left_thigh = pv.Cube(center=(-0.2, 0, -0.2), x_length=0.2, y_length=0.2, z_length=0.6)
35+
left_thigh["part_id"] = np.full(left_thigh.n_points, 3, dtype=int) # 左大腿ID=3
36+
left_calf = pv.Cube(center=(-0.2, 0, -0.8), x_length=0.2, y_length=0.2, z_length=0.6)
37+
left_calf["part_id"] = np.full(left_calf.n_points, 4, dtype=int) # 左小腿ID=4
38+
39+
# 用MultiBlock合并所有部件
40+
block = pv.MultiBlock()
41+
block.append(torso)
42+
block.append(head)
43+
block.append(right_upper_arm)
44+
block.append(right_forearm)
45+
block.append(left_upper_arm)
46+
block.append(left_forearm)
47+
block.append(right_thigh)
48+
block.append(right_calf)
49+
block.append(left_thigh)
50+
block.append(left_calf)
51+
52+
# 转换为单个UnstructuredGrid
53+
robot = block.combine()
54+
55+
# 添加关节标记(右肘、左膝)
56+
robot.point_data["joints"] = np.zeros(robot.n_points, dtype=int)
57+
# 右肘关节(右上臂+右前臂连接点)
58+
elbow_idx = robot.find_closest_point((0.7, 0, 1.0))
59+
robot.point_data["joints"][elbow_idx] = 1
60+
# 左膝关节(左大腿+左小腿连接点)
61+
knee_idx = robot.find_closest_point((-0.2, 0, -0.5))
62+
robot.point_data["joints"][knee_idx] = 1
63+
64+
if return_parts:
65+
return robot, [torso, head, right_upper_arm, right_forearm, left_upper_arm, left_forearm, right_thigh,
66+
right_calf, left_thigh, left_calf]
67+
return robot
68+
69+
70+
# 关节旋转函数(保持逻辑,适配新部件ID)
71+
def rotate_joint(robot, joint_name, angle_deg):
72+
joint_config = {
73+
"right_elbow": {
74+
"center": (0.7, 0, 1.0), # 右肘关节中心点
75+
"target_part": 2 # 右前臂ID=2
76+
},
77+
"left_knee": {
78+
"center": (-0.2, 0, -0.5), # 左膝关节中心点
79+
"target_part": 4 # 左小腿ID=4
80+
}
81+
}
82+
83+
if joint_name not in joint_config:
84+
print(f"错误:关节名称{joint_name}不存在!")
85+
return robot
86+
87+
config = joint_config[joint_name]
88+
part_mask = robot["part_id"] == config["target_part"]
89+
if not np.any(part_mask):
90+
print(f"错误:未找到part_id={config['target_part']}的部件!")
91+
return robot
92+
93+
# 旋转逻辑(与之前一致)
94+
target_points = robot.points[part_mask].copy()
95+
vtk_points = vtk.vtkPoints()
96+
for point in target_points:
97+
vtk_points.InsertNextPoint(point)
98+
99+
transform = vtk.vtkTransform()
100+
transform.Translate(config["center"][0], config["center"][1], config["center"][2])
101+
transform.RotateY(angle_deg)
102+
transform.Translate(-config["center"][0], -config["center"][1], -config["center"][2])
103+
104+
transform_filter = vtk.vtkTransformFilter()
105+
transform_filter.SetTransform(transform)
106+
poly_data = vtk.vtkPolyData()
107+
poly_data.SetPoints(vtk_points)
108+
transform_filter.SetInputData(poly_data)
109+
transform_filter.Update()
110+
111+
rotated_vtk_points = transform_filter.GetOutput().GetPoints()
112+
rotated_points = np.array([rotated_vtk_points.GetPoint(i) for i in range(rotated_vtk_points.GetNumberOfPoints())])
113+
robot.points[part_mask] = rotated_points
114+
return robot
115+
116+
117+
# 本地测试
118+
if __name__ == "__main__":
119+
robot_initial = create_robot()
120+
print(f"初始模型:{robot_initial.n_points}个点,{robot_initial.n_cells}个面")
121+
122+
# 测试右肘+左膝旋转
123+
robot_rotated = rotate_joint(robot_initial.copy(), "right_elbow", 90)
124+
robot_rotated = rotate_joint(robot_rotated, "left_knee", 60)
125+
126+
# 渲染对比
127+
plotter = pv.Plotter(shape=(1, 2), window_size=(1200, 600))
128+
# 左窗口:初始姿态
129+
plotter.subplot(0, 0)
130+
plotter.add_mesh(
131+
robot_initial,
132+
scalars="part_id",
133+
cmap="tab10", # 更多颜色区分部件
134+
edge_color="black",
135+
show_edges=True,
136+
scalar_bar_args={"title": "部件ID"}
137+
)
138+
plotter.add_axes()
139+
grid = pv.Plane(center=(0, 0, -1.2), i_size=3, j_size=3, i_resolution=10, j_resolution=10)
140+
plotter.add_mesh(grid, color="lightgray", opacity=0.5)
141+
plotter.add_text("初始人形机器人", font_size=14, position="upper_left")
142+
143+
# 右窗口:旋转后姿态
144+
plotter.subplot(0, 1)
145+
plotter.add_mesh(
146+
robot_rotated,
147+
scalars="part_id",
148+
cmap="tab10",
149+
edge_color="black",
150+
show_edges=True,
151+
scalar_bar_args={"title": "部件ID"}
152+
)
153+
plotter.add_axes()
154+
plotter.add_mesh(grid, color="lightgray", opacity=0.5)
155+
plotter.add_text("抬臂+屈膝动作", font_size=14, position="upper_left")
156+
157+
plotter.show()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html lang="zh-CN">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>具身人3D可视化与动作交互系统</title>
6+
<style>
7+
body { margin: 0; padding: 20px; background-color: #f5f5f5; font-family: Arial, sans-serif; }
8+
.page-title { text-align: center; color: #333; margin-bottom: 20px; }
9+
.btn-container { text-align: center; margin-bottom: 15px; }
10+
.control-btn { padding: 8px 16px; margin: 0 5px; border: none; border-radius: 4px; cursor: pointer; background-color: #4CAF50; color: white; }
11+
.control-btn:hover { background-color: #45a049; }
12+
#3d-container { width: 100%; height: 600px; border: 1px solid #ddd; border-radius: 8px; background-color: white; }
13+
</style>
14+
</head>
15+
<body>
16+
<h1 class="page-title">具身人3D可视化与动作交互系统</h1>
17+
18+
<div class="btn-container">
19+
<button class="control-btn" onclick="changeColor('skyblue')">蓝色</button>
20+
<button class="control-btn" onclick="changeColor('lightgreen')">绿色</button>
21+
<button class="control-btn" onclick="changeColor('pink')">粉色</button>
22+
</div>
23+
24+
<div id="3d-container"></div>
25+
26+
<script src="https://cdn.jsdelivr.net/npm/@jupyter-widgets/[email protected]/dist/embed-amd.js"></script>
27+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/pyvista.min.js"></script>
28+
<script src="{{ url_for('static', filename='js/3d_render.js') }}"></script>
29+
</body>
30+
</html>

0 commit comments

Comments
 (0)