3d实现公告牌Billboard

公告牌Billboard

在3d中,相机是可以移动和旋转角度的,可以看到物体的不同面。但是有一种特殊的存在,就是想让物体一直对着相机,能够显示出来,受相机的位置变化所影响,3d中把这个技术叫做公告牌Billboard。

原理:

动态调整平面物体的朝向,使其始终面向摄像机,保持物体与视线方向垂直。

Billboard的核心原理是:通过构造一个特殊的旋转矩阵,使平面始终面向摄像机坐标系。

就是上面说的那个特点,物体平面都是对着相机的。假设物体的前方向都是对着相机,物体对着相机的正交坐标系也是能算的。只要算出来对着相机的旋转矩阵,让物体旋转就可以了,就是这么个原理。

还有一种是圆柱形公告板,就是只能绕着圆柱轴旋转,其他那个方向不旋转。

计算思路:

P = 平面中心点(世界坐标)

C = 摄像机位置(世界坐标)

V = 视图方向向量

Up = 世界向上向量(通常是(0,1,0))

我们要构建一个旋转矩阵 R,使得平面的法向量 N 与 V 方向相同(或相反)。

步骤1:计算视线方向

物体正面对着相机的向量。

复制代码
D = C - P  // 从平面指向摄像机的向量
Z_axis = normalize(D)  // 新的前向轴(或负方向,取决于坐标系)

步骤2:构建正交坐标系

有了物体对着相机的向量,我们是能构建一个标准的正交坐标系(两两轴都垂直)。

问题:仅知道Z轴(视线方向),需要找到合适的X轴(右向量)和Y轴(上向量)。

复制代码
X_axis = normalize(cross(WorldUp, Z_axis))
Y_axis = cross(Z_axis, X_axis)

步骤3:处理退化情况

当 Z_axis 与 WorldUp 几乎平行时,叉积结果为0或接近0。

复制代码
if (|dot(Z_axis, WorldUp)| > 0.9999) {
    // 接近平行,使用备用向量
    X_axis = normalize(cross(WorldLeft, Z_axis))
    Y_axis = cross(Z_axis, X_axis)
}

步骤4:构造旋转矩阵

旋转矩阵的列向量定义了局部坐标系在世界坐标系中的方向:

复制代码
R = [ X_axis.x  Y_axis.x  Z_axis.x  0 ]
    [ X_axis.y  Y_axis.y  Z_axis.y  0 ]
    [ X_axis.z  Y_axis.z  Z_axis.z  0 ]
    [    0        0         0       1 ]

有了旋转矩阵,只要把我们物体旋转到旋转矩阵位置就可以了。

或者是把3d中的顶点数据乘以旋转矩阵,就是我们要的新的顶点数据了。

这两个方法都可以实现,一个是cpu计算,控制物体旋转,一个是gpu计算顶点数据时候直接计算。

Qt3d具体实现公告牌案例

QText2DEntity + 调整模型旋转实现

功能:

利用qt的QText2DEntity文本+ QTransform 位置,根据相机的位置变化,调整模型的位置变化。

实现不管怎么旋转,文本都显示在相机的正面视角里面。

cpp 复制代码
class BillboardTextEntity : public Qt3DCore::QEntity
{
    Q_OBJECT
public:
    BillboardTextEntity(Qt3DCore::QNode *parent = nullptr)
        : Qt3DCore::QEntity(parent)
        , m_textEntity(new Qt3DExtras::QText2DEntity(this))
        , m_transform(new Qt3DCore::QTransform(this))
    {
        // 设置文本实体
        m_textEntity->setFont(QFont("Arial", 12));
        m_textEntity->setText("Qt mY Text");
        m_textEntity->setColor(Qt::red);
        m_textEntity->setWidth(100);
        m_textEntity->setHeight(100);

        // 添加变换组件
        addComponent(m_transform);

        // 初始位置
        m_transform->setTranslation(QVector3D(0, 0, 0));

        // 初始缩放
        m_transform->setScale(0.1f);
    }

    void setCamera(Qt3DRender::QCamera *camera) {
        m_camera = camera;
        if (m_camera) {
            // 连接相机变化信号
            QObject::connect(m_camera, &Qt3DRender::QCamera::viewMatrixChanged,
                             this, &BillboardTextEntity::updateBillboard);
        }
    }

    void setText(const QString &text) {
        m_textEntity->setText(text);
    }

    void setPosition(const QVector3D &position) {
        m_position = position;
        m_transform->setTranslation(position);
        updateBillboard();
    }

private slots:
    void updateBillboard() {
        if (!m_camera) return;

        // 获取相机视图矩阵
        QMatrix4x4 viewMatrix = m_camera->viewMatrix();

        // 提取相机的旋转部分(去除平移和缩放)
        // 方法1:直接从视图矩阵提取旋转矩阵
        QMatrix4x4 rotationMatrix = viewMatrix;
        rotationMatrix(0, 3) = 0;
        rotationMatrix(1, 3) = 0;
        rotationMatrix(2, 3) = 0;
        rotationMatrix(3, 0) = 0;
        rotationMatrix(3, 1) = 0;
        rotationMatrix(3, 2) = 0;
        rotationMatrix(3, 3) = 1;

        // 转置矩阵得到逆旋转(使文本面向相机)
        rotationMatrix = rotationMatrix.transposed();

        // 设置旋转(只保留y轴旋转,用于2D文本)
        QQuaternion rotation = QQuaternion::fromRotationMatrix(rotationMatrix.toGenericMatrix<3,3>());
        m_transform->setRotation(rotation);
    }

private:
    Qt3DExtras::QText2DEntity *m_textEntity;
    Qt3DCore::QTransform *m_transform;
    Qt3DRender::QCamera *m_camera = nullptr;
    QVector3D m_position;
};

QPlaneMesh + 调整模型旋转实现

注意:QPlaneMesh 是初始的时候是水平面的,所以要叠加一个90°的旋转角。

功能:

1.一直对着相机。

2.可以自己绘制自己想要的信息,不在局限于文本显示。

自定义着色器,GPU绘制

自己重新写着色器,在GPU端更新顶点数据达到效果。

功能:

1.一直面对相机。

2.可以自己绘制自己想要的信息,不在局限于文本显示。

3.自定义着色器,在GPU完成计算。

cpp 复制代码
class DBillboardMaterial : public Qt3DRender::QMaterial
{
    Q_OBJECT
public:
    explicit DBillboardMaterial(Qt3DCore::QNode *parent = nullptr)
        : Qt3DRender::QMaterial(parent)
        , mTexture(new Qt3DRender::QTexture2D(this))
        , mCameraPosition(new Qt3DRender::QParameter("cameraPosition", QVector3D(0, 10.0f, 20.0f), this))
        , mModelPosition(new Qt3DRender::QParameter("modelPosition", QVector3D(0.0f, 1.5f, 0.0f), this))
        , mCameraUp(new Qt3DRender::QParameter("cameraUp", QVector3D(0.0f, 1.5f, 0.0f), this))
    {
        setupTexture();
        setupShader();
        setupEffect();

        // 添加参数
        addParameter(mCameraPosition);
        addParameter(mModelPosition);
        addParameter(mCameraUp);
    }

    void setTextureImage(Qt3DRender::QPaintedTextureImage *image) {
        mTexture->addTextureImage(image);
    }

    void setCameraPosition(const QVector3D &position) {
        mCameraPosition->setValue(position);
    }

    void setCameraUp(const QVector3D &up)
    {
        mCameraUp->setValue(up);
    }

    void setModelPosition(const QVector3D &position) {
        mModelPosition->setValue(position);
    }

private:
    void setupTexture() {
        mTexture->setGenerateMipMaps(false);
        mTexture->setMagnificationFilter(Qt3DRender::QAbstractTexture::Linear);
        mTexture->setMinificationFilter(Qt3DRender::QAbstractTexture::Linear);
        mTexture->setWrapMode(Qt3DRender::QTextureWrapMode(Qt3DRender::QTextureWrapMode::ClampToEdge));

        Qt3DRender::QParameter *textureParam = new Qt3DRender::QParameter("tex0", mTexture, this);
        addParameter(textureParam);
    }

    void setupShader() {
        Qt3DRender::QShaderProgram *shaderProgram = new Qt3DRender::QShaderProgram(this);

        shaderProgram->setVertexShaderCode( Qt3DRender::QShaderProgram::loadSource( QUrl( QStringLiteral( "qrc:/shaders/billboards.vert" ) ) ) );
        shaderProgram->setFragmentShaderCode( Qt3DRender::QShaderProgram::loadSource( QUrl( QStringLiteral( "qrc:/shaders/billboards.frag" ) ) ) );

        // 创建渲染通道
        Qt3DRender::QRenderPass *renderPass = new Qt3DRender::QRenderPass(this);
        renderPass->setShaderProgram(shaderProgram);

        Qt3DRender::QFilterKey *filterKey = new Qt3DRender::QFilterKey;
        filterKey->setName( QStringLiteral( "renderingStyle" ) );
        filterKey->setValue( "forward" );

        // 设置渲染状态(支持透明)
        setupRenderStates(renderPass);

        // 创建技术
        Qt3DRender::QTechnique *technique = new Qt3DRender::QTechnique(this);
        // 设置图形 API
        //setupGraphicsApi(technique);
        technique->addRenderPass(renderPass);
        technique->addFilterKey(filterKey);
        technique->graphicsApiFilter()->setApi( Qt3DRender::QGraphicsApiFilter::OpenGL );
        technique->graphicsApiFilter()->setProfile( Qt3DRender::QGraphicsApiFilter::CoreProfile );
        technique->graphicsApiFilter()->setMajorVersion( 3 );
        technique->graphicsApiFilter()->setMinorVersion( 1 );

        // 创建效果
        mEffect = new Qt3DRender::QEffect(this);
        mEffect->addTechnique(technique);
        setEffect( mEffect );
    }

    void setupRenderStates(Qt3DRender::QRenderPass *renderPass) {
        // 深度测试
        Qt3DRender::QDepthTest *depthTest = new Qt3DRender::QDepthTest(renderPass);
        depthTest->setDepthFunction(Qt3DRender::QDepthTest::Less);
        renderPass->addRenderState(depthTest);

        // Alpha 混合(支持透明)
        Qt3DRender::QBlendEquationArguments *blend = new Qt3DRender::QBlendEquationArguments(renderPass);
        blend->setSourceRgb(Qt3DRender::QBlendEquationArguments::SourceAlpha);
        blend->setDestinationRgb(Qt3DRender::QBlendEquationArguments::OneMinusSourceAlpha);
        renderPass->addRenderState(blend);

        Qt3DRender::QBlendEquation *blendEquation = new Qt3DRender::QBlendEquation(renderPass);
        blendEquation->setBlendFunction(Qt3DRender::QBlendEquation::Add);
        renderPass->addRenderState(blendEquation);

        // 背面剔除(可选)
        Qt3DRender::QCullFace *cullFace = new Qt3DRender::QCullFace(renderPass);
        cullFace->setMode(Qt3DRender::QCullFace::Back);
        renderPass->addRenderState(cullFace);
    }

    void setupGraphicsApi(Qt3DRender::QTechnique *technique) {
        Qt3DRender::QGraphicsApiFilter *apiFilter = technique->graphicsApiFilter();
        apiFilter->setApi(Qt3DRender::QGraphicsApiFilter::OpenGL);
        apiFilter->setProfile(Qt3DRender::QGraphicsApiFilter::NoProfile);
        apiFilter->setMajorVersion(3);
        apiFilter->setMinorVersion(1);
    }

    void setupEffect() {
        setEffect(mEffect);
    }

private:
    Qt3DRender::QTexture2D *mTexture;
    Qt3DRender::QParameter *mCameraPosition;
    Qt3DRender::QParameter *mModelPosition;
    Qt3DRender::QParameter *mCameraUp;
    Qt3DRender::QEffect *mEffect;
};
相关推荐
wkd_0074 小时前
【Qt | QTableWidget】QTableWidget 类的详细解析与代码实践
开发语言·qt·qtablewidget·qt5.12.12·qt表格
qq_532453535 小时前
使用 GaussianSplats3D 在 Vue 3 中构建交互式 3D 高斯点云查看器
前端·vue.js·3d
CoLiuRs5 小时前
Image-to-3D — 让 2D 图片跃然立体*
python·3d·flask
残梦53145 小时前
Qt6.9.1起一个图片服务器(支持前端跨域请求,不支持上传,可扩展)
运维·服务器·开发语言·c++·qt
mengzhi啊5 小时前
QT的语言家使用方法示范
qt
Henry Zhu1235 小时前
Qt网络编程详解(下):项目实战
网络·qt
新启航光学频率梳6 小时前
特种爆破装置传爆深孔孔深光学3D轮廓测量-激光频率梳3D轮廓技术
科技·3d·制造
轩情吖6 小时前
Qt布局管理器
开发语言·c++·qt·布局管理器·桌面级·qvboxlayout·qhboxlayout
CSDN_RTKLIB6 小时前
Qt Creator中修改源文件编码
qt
应用市场7 小时前
基于稠密对应关系的3D人体网格回归技术详解
3d·数据挖掘·回归