背景:
一个机器人示教器项目,最初希望做一个3D机器人模型动画展示,科研意义大于应用意义。从项目一开始心里真没底,只是知道qt的Quick示例中有3D机器人,要说看肯定是看不懂的,虽然能想到每个关节都类似独立的控件,都可以独立控制姿态,但也就是一说,真要实现光看示例是不够的。很多学习的动力,总得用得上才会去搞。
就如小时候问数学老师,哪怕最简单的面积和体积计算,当初古人是干啥就想起来研究这个了?老师说是因为生产过程中用到了,就想办法解决,久而久之就成了知识。实际咱们搞计算机是一样的道理。
下面记录干货,以便查阅。
首先qml是基础,不再赘述。qt的quick实际在很多qml开发场景中都用得着,qt官方的命名就是比较乱,个人认为quick可能更偏重于渲染相关。
3D基础:
任何在界面上的呈现,以前叫绘制、重绘,现在干脆叫渲染,我认为知道意思即可。
在界面上呈现3D总得有个控件,比如:View3D。
3D就像一个虚拟世界,这个世界就叫场景。比如:SceneEnvironment。
场景中得有光照,光照是分方向和距离的。比如:DirectionalLight。
再精彩的画面,也得用眼睛看。上述这些3D的场景和内容,总要通过那个控件显示到2D的显示器上,所以还要有个眼睛来指定看这些画面的方向和距离。比如:PerspectiveCamera。
在这个3D的世界中,还要划分模块,相当于子世界,用于设定相对特性。就像现实中,也得分不同的区域一样。这里就是各种Node以及其派生。我这次常用的就是Model。
到此先这样理解,去查手册会发现光照和相机也是node的派生,以后有兴趣再深究。
对于每个Model,手册里这样写的:
The Model item makes it possible to load a mesh and modify how its shaded, by adding materials to it. For a model to be renderable, it needs at least a mesh and a material.
简单说就是Model至少得有一个mesh和一个材质material。mesh就跟图片资源一样,它是一个立体的数据描述。材质是对着色的描述,如果要改变某物体的外观着色或者贴图,就要在材质里操作。
对材质的进一步定义就是纹理这一级了。
上面这些做基础就够了。大概知道3D是个啥情况即可。
可以幻想一下3D游戏里的场景,能对应上各种术语即可。
mesh:
想显示这个机器人,总得有真正的3D模型,比如SolidWorks和3Dmax制作的模型。那些东西生成的扩展名是stl之类的,不是mesh。
qt官方提供了转换工具,尤其qt6还提供了带界面的工具Balsam,这要是不看书不上网查,自己很难知道,尽管手册里有说明。因为一般不往那里想。除非逐字看手册,但这不是高效的学习方法,还是推荐看书。这个工具在咱们常用的path里,比如:D:\Qt6\6.9.1\mingw_64\bin\balsamui.exe,直接是个界面程序,不用研究命令行,可以批量转换。
balsamui转换后会生成mesh文件和一堆qml,实际我只用了mesh文件。最好有个mesh浏览器能一眼知道是什么零件,要不还要跟我一样一边做一边运行一下看效果。看qml不用总编译,使用qml预览工具即可。感兴趣可以自己做一个mesh预览工具。
预览mesh有时候可能一下看不到,往往都是因为光照、相机、缩放等不合适。比如你在桌子上放个苹果,然后手机总对着地板拍,这样咋也拍不到。或者苹果的显示单位和场景比例不协调,就如拿手机离老远拍芝麻,那没法看见。
所以最好把一些常用的参数做成可调整的,一旦运行起来,通过调整参数看实际需要的值是多少。
对于Quick系统,每个3D的初始相机拍摄位置都是从z轴往下俯视xy平面的,y轴向上,x轴向右。
下面先看一个demo。
demo:
我做好的预览界面如下:

可以看到搞了很多取值大的滑块来调整参数。省的看不到的时候没信心。
尤其调整角度的,我直接设置的正负360度,调就范围大些。
上图那个就不放动画了,可以升降,旋转,臂伸缩。
跟设计qml一样,尽量模块化处理,但也不要太琐碎。每个部件的坐标系都是相对于父级的。之前说过,每个部件都是个Model,也就是Node,想象成树结构即可。所以按模块封装很容易理解。
这个机器人我这样封装的:
最上面的手、小臂、大臂封装成Arm,通过参数指定左右手、伸缩角度。这部分大臂是跟节点,小臂是相对于大臂调整定位的,手也是。
再以最下面的基座为根,添加立柱,在立柱子节点添加臂底座,底座子节点再添加刚封装好的臂。
最后在场景qml中,也就是View3D的子节点中引用基座,一个完整的晶圆机器人就完成了。下面是qml代码:
Arm_Big.qml:
javascript
import QtQuick
import QtQuick3D
Node {
id: root
property bool _bLeftside: true
property double _dAngle_Arm: 0
eulerRotation.z: _bLeftside ? -_dAngle_Arm : _dAngle_Arm - 180
PrincipledMaterial {
id: _STL_BINARY_3
objectName: "DefaultMaterial"
}
Model {
id: _arm_big
source: "meshes/arm_big.mesh"
materials: [ _STL_BINARY_3 ]
Model {
id: _r1_small
source: "meshes/arm_small.mesh"
materials: [ _STL_BINARY_3 ]
y: 0.163
z: 0.04
eulerRotation.z: root._bLeftside ? root._dAngle_Arm * 2 - 180 : 180 - root._dAngle_Arm * 2
Model {
id: _hand
source: root._bLeftside ? "meshes/hand_left.mesh" : "meshes/hand_right.mesh"
materials: [ _STL_BINARY_3 ]
y: 0.163
z: 0.03
eulerRotation.z: root._bLeftside ? -root._dAngle_Arm : root._dAngle_Arm - 180
}
}
}
}
Robot.qml:
javascript
import QtQuick
import QtQuick3D
Node {
id: root
property double _dZ: 0
property double _dAngle_Theta: 0
property double _dAngle_R0: 0
property double _dAngle_R1: 0
PrincipledMaterial {
id: _STL_BINARY_3
objectName: "DefaultMaterial"
}
Model {
id: _body_base
source: "meshes/body_base.mesh"
materials: [ _STL_BINARY_3 ]
Model {
id: _center_bottom
source: "meshes/center_bottom.mesh"
materials: [ _STL_BINARY_3 ]
z: 0.6 + root._dZ / 100
eulerRotation.z: root._dAngle_Theta
Model {
id: _center_top
source: "meshes/center_top.mesh"
materials: [ _STL_BINARY_3 ]
z: 0.05
Arm_Big {
id: _arm_left
y: 0.07
_bLeftside: true
_dAngle_Arm: root._dAngle_R0
}
Arm_Big {
id: _arm_right
y: -0.07
_bLeftside: false
_dAngle_Arm: root._dAngle_R1
}
}
}
}
}
main.qml:
javascript
import QtQuick
import QtQuick3D
import QtQuick.Controls
import QtQuick.Layouts
import "./"
Item {
implicitWidth: 800
implicitHeight: 600
RowLayout {
anchors.fill: parent
ColumnLayout {
Layout.fillHeight: true
//sence rotation
RowLayout {
Label { text: "sence rota x" }
Slider {
id:sldr_sence_rota_x
from: -360
to: 360
value: 0
}
Label { text: sldr_sence_rota_x.value }
}
RowLayout {
Label { text: "sence rota y" }
Slider {
id:sldr_sence_rota_y
from: -360
to: 360
value: 0
}
Label { text: sldr_sence_rota_y.value }
}
RowLayout {
Label { text: "sence rota z" }
Slider {
id:sldr_sence_rota_z
from: -360
to: 360
value: 0
}
Label { text: sldr_sence_rota_z.value }
}
//camera
RowLayout {
Label { text: "camera x" }
Slider {
id:sldr_cmr_x
from: -360
to: 360
value: 0
}
Label { text: sldr_cmr_x.value }
}
RowLayout {
Label { text: "camera y" }
Slider {
id:sldr_cmr_y
from: -360
to: 360
value: 0
}
Label { text: sldr_cmr_y.value }
}
RowLayout {
Label { text: "camera z" }
Slider {
id:sldr_cmr_z
from: -360
to: 360
value: 50
}
Label { text: sldr_cmr_z.value }
}
//body
RowLayout {
Label { text: "body x" }
Slider {
id:sldr_body_x
from: -360
to: 360
value: 0
}
Label { text: sldr_body_x.value }
}
RowLayout {
Label { text: "body y" }
Slider {
id:sldr_body_y
from: -360
to: 360
value: 0
}
Label { text: sldr_body_y.value }
}
RowLayout {
Label { text: "body z" }
Slider {
id:sldr_body_z
from: -360
to: 360
value: 0
}
Label { text: sldr_body_z.value }
}
Slider {
id:sldrY
from: -360
to: 360
value: 0
}
//Arm
ColumnLayout {
Label { text: "arm_z" }
Slider {
id:sldr_arm_z
from: 0
to: 20
value: 0
}
Label { text: sldr_arm_z.value }
Label { text: "arm_theta angle" }
Slider {
id:sldr_arm_angle_theta
from: -360
to: 360
value: 0
}
Label { text: sldr_arm_angle_theta.value }
Label { text: "arm_left angle" }
Slider {
id:sldr_arm_angle_left
from: -360
to: 360
value: 0
}
Label { text: sldr_arm_angle_left.value }
Label { text: "arm_right angle" }
Slider {
id:sldr_arm_angle_right
from: -360
to: 360
value: 0
}
Label { text: sldr_arm_angle_right.value }
}
}
View3D {
Layout.fillHeight: true
Layout.fillWidth: true
camera: camera
PerspectiveCamera {
id: camera
x: sldr_cmr_x.value
y: sldr_cmr_y.value
z: sldr_cmr_z.value
}
Node {
id: scene
pivot.z: 0
eulerRotation.x: sldr_sence_rota_x.value
eulerRotation.y: sldr_sence_rota_y.value
eulerRotation.z: sldr_sence_rota_z.value
PointLight {
x: 760
z: 770
quadraticFade: 0
brightness: 1
}
DirectionalLight {
eulerRotation.z: 30
eulerRotation.y: -165
}
DirectionalLight {
y: 1000
brightness: 0.4
eulerRotation.z: -180
eulerRotation.y: 90
eulerRotation.x: -90
}
Robot {
x: sldr_body_x.value
y: sldr_body_y.value
z: sldr_body_z.value
scale: Qt.vector3d(40, 40, 40)
pivot.z: 0
eulerRotation.z: sldrY.value
_dZ: sldr_arm_z.value
_dAngle_Theta: sldr_arm_angle_theta.value
_dAngle_R0: sldr_arm_angle_left.value
_dAngle_R1: sldr_arm_angle_right.value
}
}
environment: sceneEnvironment
SceneEnvironment {
id: sceneEnvironment
antialiasingQuality: SceneEnvironment.VeryHigh
antialiasingMode: SceneEnvironment.MSAA
}
}
}
}
预览时使用带qt环境变量的命令行终端,运行qml main.qml即可。
可以看到,只要能接受qml参数,后面就不用说了。可以顺利嵌入qml项目。
题外话:
最初评估项目时,由于没接触过3D,有i两种思路:
一是看能否利用ros2系统自带的rviz,或许能够利用widget部件类,做成qml的c++扩展类,然后渲染到qml。是否可行不知道。
二是以quick示例为准,力争转换成mesh,也就是demo的方法。最终实现了。
或许有机会再想想第一种想法是否可行。
结束:
基础概念之前大概看过没实操,真正实践也就一个下午。
本文完。