【qml-10】Quick3D实现机器人渲染(mesh)记录
背景:
一个机器人示教器项目,最初希望做一个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:
import QtQuick
import QtQuick3DNode {id: rootproperty bool _bLeftside: trueproperty double _dAngle_Arm: 0eulerRotation.z: _bLeftside ? -_dAngle_Arm : _dAngle_Arm - 180PrincipledMaterial {id: _STL_BINARY_3objectName: "DefaultMaterial"}Model {id: _arm_bigsource: "meshes/arm_big.mesh"materials: [ _STL_BINARY_3 ]Model {id: _r1_smallsource: "meshes/arm_small.mesh"materials: [ _STL_BINARY_3 ]y: 0.163z: 0.04eulerRotation.z: root._bLeftside ? root._dAngle_Arm * 2 - 180 : 180 - root._dAngle_Arm * 2Model {id: _handsource: root._bLeftside ? "meshes/hand_left.mesh" : "meshes/hand_right.mesh"materials: [ _STL_BINARY_3 ]y: 0.163z: 0.03eulerRotation.z: root._bLeftside ? -root._dAngle_Arm : root._dAngle_Arm - 180}}}
}
Robot.qml:
import QtQuick
import QtQuick3DNode {id: rootproperty double _dZ: 0property double _dAngle_Theta: 0property double _dAngle_R0: 0property double _dAngle_R1: 0PrincipledMaterial {id: _STL_BINARY_3objectName: "DefaultMaterial"}Model {id: _body_basesource: "meshes/body_base.mesh"materials: [ _STL_BINARY_3 ]Model {id: _center_bottomsource: "meshes/center_bottom.mesh"materials: [ _STL_BINARY_3 ]z: 0.6 + root._dZ / 100eulerRotation.z: root._dAngle_ThetaModel {id: _center_topsource: "meshes/center_top.mesh"materials: [ _STL_BINARY_3 ]z: 0.05Arm_Big {id: _arm_lefty: 0.07_bLeftside: true_dAngle_Arm: root._dAngle_R0}Arm_Big {id: _arm_righty: -0.07_bLeftside: false_dAngle_Arm: root._dAngle_R1}}}}
}
main.qml:
import QtQuick
import QtQuick3D
import QtQuick.Controls
import QtQuick.Layouts
import "./"Item {implicitWidth: 800implicitHeight: 600RowLayout {anchors.fill: parentColumnLayout {Layout.fillHeight: true//sence rotationRowLayout {Label { text: "sence rota x" }Slider {id:sldr_sence_rota_xfrom: -360to: 360value: 0}Label { text: sldr_sence_rota_x.value }}RowLayout {Label { text: "sence rota y" }Slider {id:sldr_sence_rota_yfrom: -360to: 360value: 0}Label { text: sldr_sence_rota_y.value }}RowLayout {Label { text: "sence rota z" }Slider {id:sldr_sence_rota_zfrom: -360to: 360value: 0}Label { text: sldr_sence_rota_z.value }}//cameraRowLayout {Label { text: "camera x" }Slider {id:sldr_cmr_xfrom: -360to: 360value: 0}Label { text: sldr_cmr_x.value }}RowLayout {Label { text: "camera y" }Slider {id:sldr_cmr_yfrom: -360to: 360value: 0}Label { text: sldr_cmr_y.value }}RowLayout {Label { text: "camera z" }Slider {id:sldr_cmr_zfrom: -360to: 360value: 50}Label { text: sldr_cmr_z.value }}//bodyRowLayout {Label { text: "body x" }Slider {id:sldr_body_xfrom: -360to: 360value: 0}Label { text: sldr_body_x.value }}RowLayout {Label { text: "body y" }Slider {id:sldr_body_yfrom: -360to: 360value: 0}Label { text: sldr_body_y.value }}RowLayout {Label { text: "body z" }Slider {id:sldr_body_zfrom: -360to: 360value: 0}Label { text: sldr_body_z.value }}Slider {id:sldrYfrom: -360to: 360value: 0}//ArmColumnLayout {Label { text: "arm_z" }Slider {id:sldr_arm_zfrom: 0to: 20value: 0}Label { text: sldr_arm_z.value }Label { text: "arm_theta angle" }Slider {id:sldr_arm_angle_thetafrom: -360to: 360value: 0}Label { text: sldr_arm_angle_theta.value }Label { text: "arm_left angle" }Slider {id:sldr_arm_angle_leftfrom: -360to: 360value: 0}Label { text: sldr_arm_angle_left.value }Label { text: "arm_right angle" }Slider {id:sldr_arm_angle_rightfrom: -360to: 360value: 0}Label { text: sldr_arm_angle_right.value }}}View3D {Layout.fillHeight: trueLayout.fillWidth: truecamera: cameraPerspectiveCamera {id: camerax: sldr_cmr_x.valuey: sldr_cmr_y.valuez: sldr_cmr_z.value}Node {id: scenepivot.z: 0eulerRotation.x: sldr_sence_rota_x.valueeulerRotation.y: sldr_sence_rota_y.valueeulerRotation.z: sldr_sence_rota_z.valuePointLight {x: 760z: 770quadraticFade: 0brightness: 1}DirectionalLight {eulerRotation.z: 30eulerRotation.y: -165}DirectionalLight {y: 1000brightness: 0.4eulerRotation.z: -180eulerRotation.y: 90eulerRotation.x: -90}Robot {x: sldr_body_x.valuey: sldr_body_y.valuez: sldr_body_z.valuescale: Qt.vector3d(40, 40, 40)pivot.z: 0eulerRotation.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: sceneEnvironmentSceneEnvironment {id: sceneEnvironmentantialiasingQuality: SceneEnvironment.VeryHighantialiasingMode: SceneEnvironment.MSAA}}}
}
预览时使用带qt环境变量的命令行终端,运行qml main.qml即可。
可以看到,只要能接受qml参数,后面就不用说了。可以顺利嵌入qml项目。
题外话:
最初评估项目时,由于没接触过3D,有i两种思路:
一是看能否利用ros2系统自带的rviz,或许能够利用widget部件类,做成qml的c++扩展类,然后渲染到qml。是否可行不知道。
二是以quick示例为准,力争转换成mesh,也就是demo的方法。最终实现了。
或许有机会再想想第一种想法是否可行。
结束:
基础概念之前大概看过没实操,真正实践也就一个下午。
本文完。