1.前言

之前是使用Coin3D来绘制机械手场景的【Qt利用Coin3D(OpenInventor)进行3d绘图】
后来需要在HarmonyOS显示这个机械手模型,但是想要编译Coin3D到HarmonyOS的话,显然太难了。
然后尝试使用OpenGL原生的函数来绘制,但是HarmonyOS对很多函数都不支持,一查,发现HarmonyOS支持OpenglES。那就麻烦了。
最后,一番兜兜转转,还是用回Qt3D吧。

2.效果

效果也还行,但是某些功能还得研究一下。
在这里插入图片描述点、线、面、灯光等各种功能都ok。其中点线面的渲染是通过设置 GeometryRenderervoid setPrimitiveType(Qt3DRender::QGeometryRenderer::PrimitiveType primitiveType)实现的。

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/8c52fb4533714e9588d87d3ac7d45ca5.png在这里插入图片描述

3.实现过程

主要是参考了Qt自带的例程以及网上的一些资料。

3.1.场景代码

创建场景、摄像机、灯光等这些基本元素的代码,是参考Qt自带的例程实现的。比如下面的【Qt 3D Simple C++ Example】在这里插入图片描述

3.2.自定义模型的渲染

【Qt3DExample】

3.3.绘制直线或者网格

【how-do-i-draw-a-simple-line-in-Qt3D】

int drawOriginGrid(const int rows,
                   const int cols,
                   const float step,
                   const QColor& color,
                   Qt3DCore::QEntity *_rootEntity)
{
    auto *geometry = new Qt3DRender::QGeometry(_rootEntity);

    QList<QVector3D> vertexList;
    // 横线
    for(int i = -rows; i <= rows; i++)
    {
        vertexList << QVector3D(cols  * step, 0, i * step);
        vertexList << QVector3D(-cols * step, 0, i * step);
    }
    // 竖线
    for(int i = -cols; i <= cols; i++)
    {
        vertexList << QVector3D(i * step, 0, rows * step);
        vertexList << QVector3D(i * step, 0, -rows * step);
    }
    QByteArray bufferBytes;
    bufferBytes.resize(3 * vertexList.length() * sizeof(float));
    float *positions = reinterpret_cast<float*>(bufferBytes.data());

    for(int i = 0; i < vertexList.length(); i++)
    {
        QVector3D vertex = vertexList.at(i);
        *positions++ = vertex.x();
        *positions++ = vertex.y();
        *positions++ = vertex.z();
    }

    auto *buf = new Qt3DRender::QBuffer(geometry);
    buf->setData(bufferBytes);

    auto *positionAttribute = new Qt3DRender::QAttribute(geometry);
    positionAttribute->setName(Qt3DRender::QAttribute::defaultPositionAttributeName());
    positionAttribute->setVertexBaseType(Qt3DRender::QAttribute::Float);
    positionAttribute->setVertexSize(3);
    positionAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute);
    positionAttribute->setBuffer(buf);
    positionAttribute->setByteStride(3 * sizeof(float));
    positionAttribute->setCount(vertexList.length());
    geometry->addAttribute(positionAttribute); // We add the vertices in the geometry

    // connectivity between vertices
    QByteArray indexBytes;
    indexBytes.resize(2 * vertexList.length() * sizeof(unsigned int));
    unsigned int *indices = reinterpret_cast<unsigned int*>(indexBytes.data());
    for(int i = 0; i < vertexList.length(); i++)
    {
        *indices++ = i;
    }

    auto *indexBuffer = new Qt3DRender::QBuffer(geometry);
    indexBuffer->setData(indexBytes);

    auto *indexAttribute = new Qt3DRender::QAttribute(geometry);
    indexAttribute->setVertexBaseType(Qt3DRender::QAttribute::UnsignedInt);
    indexAttribute->setAttributeType(Qt3DRender::QAttribute::IndexAttribute);
    indexAttribute->setBuffer(indexBuffer);
    indexAttribute->setCount(vertexList.length());
    geometry->addAttribute(indexAttribute); // We add the indices linking the points in the geometry

    // mesh
    auto *line = new Qt3DRender::QGeometryRenderer(_rootEntity);
    line->setGeometry(geometry);
    line->setPrimitiveType(Qt3DRender::QGeometryRenderer::Lines);
    auto *material = new Qt3DExtras::QPhongMaterial(_rootEntity);
    material->setAmbient(color);
    material->setDiffuse(color);

    // entity
    auto *lineEntity = new Qt3DCore::QEntity(_rootEntity);
    lineEntity->addComponent(line);
    lineEntity->addComponent(material);

    return 0;
}

3.3.贴图

3.3.1.从文件加载

        Qt3DRender::QTextureImage *txtImg = new Qt3DRender::QTextureImage();
        txtImg->setSource("file:///" + QDir::currentPath() + "/12-elefante/elefantefull.png");
        Qt3DRender::QTexture2D *txt2d = new Qt3DRender::QTexture2D();
        txt2d->addTextureImage(txtImg);
        Qt3DExtras::QDiffuseMapMaterial *material = new Qt3DExtras::QDiffuseMapMaterial();
        material->setAmbient(QColor(128, 128, 128));
        material->setShininess(0.1);
        material->setDiffuse(txt2d);

3.3.2.从内存加载

【How can I load a QPaintedTextureImage into a QTextureMaterial?】

class MyQPaintedTextureImage : public Qt3DRender::QPaintedTextureImage
{
public:
    void setImage(QImage &i){
        image = i;
        setSize(i.size());
    }
    virtual void paint(QPainter *painter) override{
        painter->drawImage(0, 0, image);
    }

private:
    QImage image;
};

// 利用QPaintedTextureImage可以实现加载内存中的图片
                const unsigned char* data = reinterpret_cast<const unsigned char*>(embTexture->pcData);
                const size_t size = embTexture->mWidth;
                QByteArray imageData((char*)data, size);
                QImage img;
                if(img.loadFromData(imageData)){
                    qDebug() << img;
                    MyQPaintedTextureImage *txtImg = new MyQPaintedTextureImage();
                    txtImg->setImage(img);

                    Qt3DRender::QTexture2D *txt2d = new Qt3DRender::QTexture2D();
                    txt2d->addTextureImage(txtImg);
                    // qMaterial->setBaseColor(QVariant::fromValue(txt2d));
                    qMaterial->setDiffuse(txt2d);
                    
									txtImg->update(); // 最好update一下,否则有些模型会导致程序直接崩溃
                }

4.有待解决的一些问题

4.1.线宽的设置、背面消隐(culling)的设置

有资料说可以通过QRenderStateSet来设置,但是我设置了并没有得到有效的效果

    Qt3DExtras::Qt3DWindow *view = new Qt3DExtras::Qt3DWindow();
    view->defaultFrameGraph()->setClearColor(QColor(QRgb(0x2b2b2b)));

    if(0)
    {
        using namespace Qt3DRender;

        qDebug() << "frameGraph:" << view->defaultFrameGraph() << view->activeFrameGraph();

        Qt3DRender::QRenderSurfaceSelector *surSel;
        auto childNodeList = view->activeFrameGraph()->childNodes();
        foreach (Qt3DCore::QNode * childNode, childNodeList) {
            qDebug() << "child:" << childNode;
            if(qobject_cast<Qt3DRender::QRenderSurfaceSelector*>(childNode))
            {
                surSel = qobject_cast<Qt3DRender::QRenderSurfaceSelector*>(childNode);
            }
        }
        qDebug() << surSel->childNodes() << surSel->childNodes()[0]->childNodes()
                 << surSel->childNodes()[0]->childNodes()[0]->childNodes();

        qDebug() << surSel->childNodes()[0]->childNodes()[0]->childNodes()[0];

        ((Qt3DRender::QClearBuffers*)(surSel->childNodes()[0]->childNodes()[0]->childNodes()[0]))
            ->setBuffers(QClearBuffers::ColorDepthBuffer);

        QRenderStateSet *renderStateSet = new QRenderStateSet(surSel->childNodes()[0]->childNodes()[0]);
        QCullFace *cullFace = new QCullFace();
        cullFace->setMode(QCullFace::NoCulling);
        // renderStateSet->addRenderState(cullFace);

        QLineWidth *lineWidth = new QLineWidth();
        lineWidth->setValue(100);
        // renderStateSet->addRenderState(lineWidth);

        QPointSize *ptSize = new QPointSize();
        ptSize->setValue(100);
        // renderStateSet->addRenderState(ptSize);

        qDebug() << surSel->childNodes()[0]->childNodes()[0]->childNodes();
    }

4.2.法线的问题

目前好像是支持点法线,不支持面法线,假如使用index模式来渲染面片时,会渲染出奇怪的效果
在这里插入图片描述
在这里插入图片描述

5.注意事项

5.1.QMesh的模型路径问题

假如使用QMesh来加载模型,记得传递的路径是带前缀的,比如

file:///E:/zhongyong/zyQt/Robot/Qt3DRobotTest/12-elefante/elefante.obj"
qrc:/assets/obj/toyplane.obj

而不能直接使用绝对路径或者相对路径。

5.2.Solidworks导出OBJ格式模型

【SolidWorks2021导出带材质的OBJ文件】
【Free-Solidworks-OBJ-Exporter】
在这里插入图片描述

5.3.关于Qt3DRender::QGeometry的Qt3DRender::QAttribute::IndexAttribute

这个IndexAttribute 属性,当所提供的顶点数据是完整时,可以不加这个;当顶点数据是复用时,就必须要有这个,否则绘制的线、面会少。

5.4.设置渲染后端

可以手动设置渲染后端

    // 设置 渲染backend
    QSurfaceFormat format;
    format.setVersion(3, 0); // 设置版本
    // format.setProfile(QSurfaceFormat::CompatibilityProfile); // 设置兼容性配置文件
    format.setProfile(QSurfaceFormat::CoreProfile); // 设置兼容性配置文件
    // format.setRenderableType(QSurfaceFormat::OpenGLES); // 设置为 OpenGL ES
    format.setRenderableType(QSurfaceFormat::OpenGL); // 设置为 OpenGL
    QSurfaceFormat::setDefaultFormat(format);

5.5.读取fbx、gltf等格式的文件

使用assimp来读取,然后再将assimp的数据结构装成Qt3D的结构
assimp使用vcpkg来编译,方便。
具体请查看这里【使用Assimp加载glb/gltf文件,然后使用Qt3D来渲染】
加载完模型后,可以用下面的代码这样对每条轴进行控制。

// 机械手模型
    if(1)
    {
        ModelLoader::loadModelFromFile(rootEntity, filePath);

        qDebug() << "---->" << rootEntity->findChild<Qt3DCore::QEntity*>("joint1") << rootEntity->components().length();
        qDebug() << rootEntity->findChildren<Qt3DCore::QEntity*>(QRegularExpression("joint1"));

        QList<Qt3DCore::QEntity*> jointList;
        for(int i = 0; i < 6; i++)
        {
            QString keyWord = QString("joint%1").arg(i + 1);
            QList<Qt3DCore::QEntity*> tmpChildList = rootEntity->findChildren<Qt3DCore::QEntity*>(QRegularExpression(keyWord));
            if(tmpChildList.isEmpty() == false)
            {
                Qt3DCore::QEntity* joint = tmpChildList.first();

                jointList << joint;
                qDebug() << "joint comps:" << joint->components().length();

                QString info = joint->objectName().split("_").last();
                info = info.toLower();

                double direction = 1.0;
                if(info.startsWith("-"))
                {
                    direction = -1.0;
                    info = info.remove(0, 1);
                }

                int motionType = 0;
                if(info.startsWith("r"))
                {
                    motionType = 0;
                }
                else if(info.startsWith("t"))
                {
                    motionType = 1;
                }
                info = info.remove(0, 1);

                int axis = 0;
                if(info.startsWith("x"))
                {
                    axis = 0;
                }
                else if(info.startsWith("y"))
                {
                    axis = 1;
                }
                else if(info.startsWith("z"))
                {
                    axis = 2;
                }

                // 目前只处理旋转,有时间再搞其他运动
                auto compVec = joint->componentsOfType<Qt3DCore::QTransform>();
                Qt3DCore::QTransform* transform = nullptr;
                if(compVec.length() > 0)
                {
                    transform = compVec.at(0);
                    qDebug() << "the trans:" << transform;
                }
                if(transform != nullptr)
                {
                    switch (axis) {
                    case 0:
                        mJointStateFuncList << [=](double value){
                             transform->setRotationX(value * direction);
                        };
                        break;
                    case 1:
                        mJointStateFuncList << [=](double value){
                            transform->setRotationY(value * direction);
                        };
                        break;
                    case 2:
                        mJointStateFuncList << [=](double value){
                            transform->setRotationZ(value * direction);
                        };
                        break;
                    default:
                        break;
                    }
                }

            }
        }
        qDebug() << jointList;

        // 找到模型的"tool"节点,在上面绘制坐标轴
        QList<Qt3DCore::QEntity*> tmpChildList = rootEntity->findChildren<Qt3DCore::QEntity*>(QRegularExpression("tool"));
        if(tmpChildList.isEmpty() == false)
        {
            Qt3DCore::QEntity* tool = tmpChildList.first();
            Qt3DCore::QEntity *toolEntity = new Qt3DCore::QEntity(tool);
            Qt3DCore::QTransform *toolTransform = new Qt3DCore::QTransform;
            // toolTransform->setTranslation(QVector3D(7.8f, 0.0f, 0.0f));
            // toolTransform->setRotationY(90);
            toolEntity->addComponent(toolTransform);
            drawLine({0, 0, 0}, {10, 0, 0}, Qt::red,   toolEntity); // X
            drawLine({0, 0, 0}, {0, 10, 0}, Qt::green, toolEntity); // Y
            drawLine({0, 0, 0}, {0, 0, 10},  Qt::blue, toolEntity); // Z
        }
    }

因为我的模型中每个节点是以【关节_正反转 运动方式 轴】的方式来命名的,比如 joint1_rz joint2_ry joint5_-ry 等等。所以就可以根据这个信息,先用QObject的findChildren函数,利用基本名称找到节点,并提取其完整名称。然后得到的信息来构造匿名函数,并把匿名函数存储到一个QList<std::function<void(float)>> mJointStateFuncList容器中。到时要控制哪一个轴,就调用对应的匿名函数即可。
其中tool节点是一个空物体,用来表示机械手末端工具的原点。
在这里插入图片描述

5.6.在场景之上绘制控件

假如想利用QWidget::createWindowContainer包裹住Qt3DExtras::Qt3DWindow,然后再在这个widget上添加控件的话,是不行的,因为这个不透明widget会无视堆叠顺序,一直保持在最顶层,把你添加的控件全都盖住。
在这里插入图片描述针对这个问题,有两个解决办法:

5.6.1.利用QOpenGLWidget封装

在github上,有人利用QOpenGLWidget实现了一个能用的类: 【florianblume/qt3d-widget】
不过我还没测试过,因为我的程序需要放到嵌入式设备中用,用QOpenGLWidget貌似有些兼容性的问题。

5.6.2.利用qml封装

幸运的是,qml中的qt3d没有这个限制,可能与qml原生支持opengl/openglES有关?
在这里插入图片描述在c++中创建的Qt3DCore::QEntity对象可以直接拿到qml中去使用
在这里插入图片描述
MyScene.qml

import QtQuick 2.0
import QtQuick.Window 2.2
import QtQuick.Scene3D 2.0

import QtQuick.Controls 2.15

import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.15

Item {
    id: root

    Scene3D {
        id: myScene
        anchors.fill: parent
        cameraAspectRatioMode: Scene3D.AutomaticAspectRatio
        focus: true
        enabled: true

        aspects: ["render", "logic", "input"]

        Entity {
            id: sceneRoot

            components: [
                RenderSettings {
                    activeFrameGraph: ForwardRenderer {
                        // clearColor: Qt.rgba(0, 0.5, 1, 1)
                        clearColor: "#2b2b2b"
                        // camera: camera
                        camera: sceneCamera
                        showDebugOverlay: false // Qt3d profiling的显示与否
                    }
                },
                // Event Source will be set by the Qt3DQuickWindow
                InputSettings { }
            ]

            Component.onCompleted: {
                // 将entity等元素抢夺过来,添加到场景中
                modelEntity.parent = sceneRoot
                sceneCamera.parent = sceneRoot
            }

        }

    }
}

5.6.3.利用qml + QQuickWidget封装

这个效果应该挺符合使用QWidget人员的需求的。
在这里插入图片描述

    QQuickWidget *view = new QQuickWidget;
    view->setSource(QUrl("qrc:/qml/MyScene.qml"));
    view->setResizeMode(QQuickWidget::SizeRootObjectToView);
    view->resize(800, 600);
    view->show();

    auto btn = new QPushButton(view);
    btn->move(100, 100);
    btn->setText("-----这是按钮-----");
    btn->show();

    auto widget = new QWidget(view);
    widget->move(300, 100);
    widget->resize(300, 200);
    widget->setStyleSheet("background-color: rgba(170, 0, 0, 128);");
    widget->show();

    auto bar = new QProgressBar(view);
    bar->move(300, 500);
    bar->resize(300, 30);
    bar->setValue(50);
    bar->show();

5.6.线程冲突导致的Qt3D显示控件无法显示且阻塞

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

// 这种方式会导致线程阻塞,从而3d控件无法显示出来或者显示出来后卡住
    // QtConcurrent::run([=](){
    //     while (1) {
    //         QThread::msleep(100);
    //         qDebug() << "----" << QDateTime::currentDateTime();
    //     }
    // });

// 这种方式就不会导致卡住
    QThread* thread = QThread::create([]() {
        while (1) {
            QThread::msleep(100);
            qDebug() << "----" << QDateTime::currentDateTime();
        }
    });
    // thread->setPriority(QThread::LowPriority);
    thread->start();

    auto mView = new Qt3DExtras::Qt3DWindow();
    mView->resize(640, 480);
    mView->show();

    return a.exec();
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐