[OpenGL] Billboard公告牌技术

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ZJU_fish1996/article/details/84035220

        billboard技术是一个相对简单,而在3D游戏中又比较常用的技术。由于自己也在不少地方需要涉及到,花了点时间做了一版billboard的功能。在这篇文章中大致描述一下自己的实现细节。

概念引入

        简单来说,billboard就是使用平面(面片)进行渲染,且使其始终朝向摄像机的一种技术。

        可以看出,billboard和相机的transform属性有着比较紧密的联系,它的关键点在于图形的空间位置变换,所以主要的工作量都在顶点着色器中完成。对于这个问题,我们有两个想法,一个是根据实际的需求修改View矩阵(将物体从世界空间转换到相机空间),也就是使用一个NewViewMatrix参与运算。另一个思路是,在有些系统对viewmatrix的封装比较严密的,我们可以取得当前的viewmatrix,在它的基础上“多”乘以一个校正矩阵,如把物体旋转到和相机的方向一致。

        两者最终的运算结果是一致的,只不过是出发点不太一样,下文中的样例代码是基于第一种思路的。

        首先,需要回顾一下,对于一般物体而言,它的顶点着色器变换一般是这样:

   gl_Position = ProjectMatrix * (ViewMatrix * (ModelMatrix * a_position));

       其中,ModelMatrix是将物体从模型空间转换到世界空间的矩阵;ViewMatrix 是将物体从世界空间转换到相机空间的矩阵;ProjectMatrix 是将物体从物体从相机空间转换到投影空间的矩阵。在接下来的计算中,我们需要弄点花样的就是这个ViewMatrix。

 

(一) 面向世界的billboard

         此类billboard的特点是,无论怎么旋转视角,它都面向摄像机;但大小会随着远近而变化。

         该demo用的这个树的面片并不太合适(但是懒得换图),可能会造成一些误解。个人觉得适用于用一个面片渲染天空的太阳、月亮之类的效果。

         关于实现这个效果,一开始最直观的想法是:只要让这个物体初始位置面向相机,不进行额外的旋转操作,只计算它自己的位移、缩放信息,再加上相机的位置就可以了。

    QVector3D upDir(0, 1, 0);

    QVector3D N = Camera::Inst()->GetCameraPos() - Camera::Inst()->GetLookAtPos();
    QVector3D U = QVector3D::crossProduct(upDir, N);
    QVector3D V = QVector3D::crossProduct(N, U);

    N.normalize();
    U.normalize();
    V.normalize();

    matrix.setRow(0, {U.x(), U.y(), U.z(), -QVector3D::dotProduct(U, Camera::Inst()->GetCameraPos())}); // x
    matrix.setRow(1, {V.x(), V.y(), V.z(), -QVector3D::dotProduct(V, Camera::Inst()->GetCameraPos())}); // y
    matrix.setRow(2, {N.x(), N.y(), N.z(), -QVector3D::dotProduct(N, Camera::Inst()->GetCameraPos())}); // z
    matrix.setRow(3, {0, 0, 0, 1});

        此处除了没有旋转外几乎没有差别。

        如果采用校正矩阵的方式,那么此处的思路是:取原始的View矩阵,将位移信息清空为0(即第四列的前三个数据),并且将原始View矩阵转置。(原本是需要求逆,但是比较特别的是,它是一个正交矩阵)

(二) 某一轴固定的billboard(以y轴为例)

        此例适用于距离比较远的树木之类的物体。它的特点是,在某一轴旋转固定,其它轴自由,比较常用的是y轴固定。最终的效果就是上下移动镜头的时候,可以看出这个图形是一个面片,不过游戏中一般都会限制上下旋转的角度,不会出现过于夸张的现象。

        这相当于对于该树木,相机只进行x轴和z轴的旋转,而不进行y轴的旋转。由于示例中原本只考虑了x轴和y轴的旋转,所以对于相机旋转,仅仅乘上x轴的旋转量。

        如果使用校正矩阵的方式,需要从view矩阵中解析出绕x,y,z轴的信息,可以使用类似于DirectX中的Decompose函数进行解算。校正矩阵中包含抵消y轴的旋转的信息(相当于再转回来)。

    QVector3D upDir(0, 1, 0);

    QVector3D N = Camera::Inst()->GetCameraPos() - Camera::Inst()->GetLookAtPos();
    QVector3D U = QVector3D::crossProduct(upDir, N);
    QVector3D V = QVector3D::crossProduct(N, U);

    N.normalize();
    U.normalize();
    V.normalize();

    matrix.setRow(0, {U.x(), U.y(), U.z(), -QVector3D::dotProduct(U,Camera::Inst()->GetCameraPos())}); // x
    matrix.setRow(1, {V.x(), V.y(), V.z(), -QVector3D::dotProduct(V,Camera::Inst()->GetCameraPos())}); // y
    matrix.setRow(2, {N.x(), N.y(), N.z(), -QVector3D::dotProduct(N,Camera::Inst()->GetCameraPos())}); // z
    matrix.setRow(3, {0, 0, 0, 1});

    matrix.rotate(Camera::Inst()->GetRotateX(), QVector3D(1,0,0));

(三) 提示信息类的billboard

        用于放置在场景中的一些提示信息。它的特点是随着相机旋转总是面向相机,且大小不随着镜头远近变化。

       和第一种情况比,不一样的地方是需要限制物体在相机空间的深度。根据需求,我们把相机平移的z值设置为一个合适的定值:

void Water::GetViewMatrix(QMatrix4x4& matrix)
{
    QVector3D upDir(0, 1, 0);

    QVector3D N = Camera::Inst()->GetCameraPos() - Camera::Inst()->GetLookAtPos();
    QVector3D U = QVector3D::crossProduct(upDir, N);
    QVector3D V = QVector3D::crossProduct(N, U);

    N.normalize();
    U.normalize();
    V.normalize();

    matrix.setRow(0, {U.x(), U.y(), U.z(), -QVector3D::dotProduct(U,Camera::Inst()->GetCameraPos())}); // x
    matrix.setRow(1, {V.x(), V.y(), V.z(), -QVector3D::dotProduct(V,Camera::Inst()->GetCameraPos())}); // y
    matrix.setRow(2, {N.x(), N.y(), N.z(), -QVector3D::dotProduct(N, QVector3D(0,0,20))}); // z
    matrix.setRow(3, {0, 0, 0, 1});
}

        同样的,如果求校正矩阵,只需要在第一种billboard的基础上校正相机的z轴平移,此处要计算相机到物体的深度。

附录:一般情况下的ViewMatrix计算

        在此处对相机矩阵的计算推导并不做阐释,本段主要是放出一般情况下的计算代码,方便进行对比和拓展。

CameraManager.h


class Camera
{
private:
    float rotateX = 0.0f;
    float rotateY = 0.0f;
    QVector3D eyeLocation = QVector3D(0, 0, 20);
    QVector3D lookAtLocation = QVector3D(0, 0, 0);
    QMatrix4x4 viewMatrix;
    Camera();

public:
    static Camera* camera;
    static Camera* Inst() {
        if(!camera) {
            camera = new Camera();
        }
        return camera;
    }

    void MoveLeft(float step);
    void MoveRight(float step);
    void MoveFront(float step);
    void MoveBack(float step);
    void MoveUp(float step);
    void MoveDown(float step);

    void SetRotateX(float x) { rotateX = x; }
    void SetRotateY(float y) { rotateY = y; }
    float GetRotateX() { return rotateX;}
    void UpdateViewMatrix();
    QVector3D GetViewDir() { return lookAtLocation - eyeLocation; }
    QMatrix4x4& GetViewMatrix() { return viewMatrix; }
    QVector3D GetCameraPos() { return eyeLocation; }
    QVector3D GetLookAtPos() { return lookAtLocation; }
};

#endif // CAMERAMANAGER_H

CameraManager.cpp

#include "cameramanager.h"

Camera* Camera::camera = nullptr;

Camera::Camera()
{
    UpdateViewMatrix();
}

void Camera::UpdateViewMatrix()
{
    QVector3D upDir(0, 1, 0);

    QVector3D N = eyeLocation - lookAtLocation; // 这里是和OpenGL的z轴方向保持一致
    QVector3D U = QVector3D::crossProduct(upDir, N);
    QVector3D V = QVector3D::crossProduct(N, U);

    N.normalize();
    U.normalize();
    V.normalize();

    viewMatrix.setRow(0, {U.x(), U.y(), U.z(), -QVector3D::dotProduct(U, eyeLocation)}); // x
    viewMatrix.setRow(1, {V.x(), V.y(), V.z(), -QVector3D::dotProduct(V, eyeLocation)}); // y
    viewMatrix.setRow(2, {N.x(), N.y(), N.z(), -QVector3D::dotProduct(N, eyeLocation)}); // z
    viewMatrix.setRow(3, {0, 0, 0, 1});


    viewMatrix.rotate(rotateX, QVector3D(1,0,0));
    viewMatrix.rotate(rotateY, QVector3D(0,1,0));
}

void Camera::MoveLeft(float step)
{
    eyeLocation.setX(eyeLocation.x() - step);
    lookAtLocation.setX(lookAtLocation.x() - step);
    UpdateViewMatrix();
}

void Camera::MoveRight(float step)
{
    eyeLocation.setX(eyeLocation.x() + step);
    lookAtLocation.setX(lookAtLocation.x() + step);
    UpdateViewMatrix();
}

void Camera::MoveFront(float step)
{
    eyeLocation.setZ(eyeLocation.z() - step);
    lookAtLocation.setZ(lookAtLocation.z() - step);
    UpdateViewMatrix();
}

void Camera::MoveBack(float step)
{
    eyeLocation.setZ(eyeLocation.z() + step);
    lookAtLocation.setZ(lookAtLocation.z() + step);
    UpdateViewMatrix();
}

void Camera::MoveUp(float step)
{
    eyeLocation.setY(eyeLocation.y() + step);
    lookAtLocation.setY(lookAtLocation.y() + step);
    UpdateViewMatrix();
}

void Camera::MoveDown(float step)
{
    eyeLocation.setY(eyeLocation.y() - step);
    lookAtLocation.setY(lookAtLocation.y() - step);
    UpdateViewMatrix();
}

MainWidget.h

#ifndef MAINWIDGET_H
#define MAINWIDGET_H


#include <QOpenGLWidget>

#include "tree.h"
#include "geometryengine.h"
#include "rendercommon.h"
#include "skybox.h"
#include <QOpenGLFunctions>
#include <QBasicTimer>

class GeometryEngine;


class MainWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
    Q_OBJECT

public:
    explicit MainWidget(QWidget *parent = nullptr);
    ~MainWidget() override;

protected:
    void mousePressEvent(QMouseEvent* event) override;
    void mouseMoveEvent(QMouseEvent* event) override;
    void mouseReleaseEvent(QMouseEvent* event) override;
    void keyPressEvent(QKeyEvent* event) override;
    void initializeGL() override;
    void resizeGL(int w, int h) override;
    void paintGL() override;


private:
    float mx = 0,my = 0,ax = 0,ay = 0;
    bool isDown = false;
    SkyBox skyBox;
    Tree tree;

};

#endif // MAINWIDGET_H

MainWidget.cpp

#include "mainwidget.h"
#include "cameramanager.h"
#include <QMouseEvent>
#include <math.h>
#include <qDebug>

MainWidget::MainWidget(QWidget *parent) :
    QOpenGLWidget(parent)
{

}

MainWidget::~MainWidget()
{

}

void MainWidget::keyPressEvent(QKeyEvent* event)
{
    const float step = 0.3f;
    if(event->key() == Qt::Key_W)
    {
        Camera::Inst()->MoveFront(step);
        update();
    }
    else if(event->key() == Qt::Key_S)
    {
        Camera::Inst()->MoveBack(step);
        update();
    }
    else if(event->key() == Qt::Key_A)
    {
        Camera::Inst()->MoveLeft(step);
        update();
    }
    else if(event->key() == Qt::Key_D)
    {
        Camera::Inst()->MoveRight(step);
        update();
    }
    else if(event->key() == Qt::Key_Q)
    {
        Camera::Inst()->MoveUp(step);
        update();
    }
    else if(event->key() == Qt::Key_E)
    {
        Camera::Inst()->MoveDown(step);
        update();
    }
}

void MainWidget::mousePressEvent(QMouseEvent *e)
{
    isDown = true;
    float x = e->x();
    float y = e->y();
    mx = x;
    my = y;

    update();
}

void MainWidget::mouseMoveEvent(QMouseEvent *e)
{
    if(isDown){
        float x = e->x();
        float y = e->y();
        ax += (y-my)/5.0f;
        ay += (x-mx)/5.0f;
        mx = x;
        my = y;
        Camera::Inst()->SetRotateX(ax);
        Camera::Inst()->SetRotateY(ay);
        Camera::Inst()->UpdateViewMatrix();
        update();
    }
}

void MainWidget::mouseReleaseEvent(QMouseEvent *e)
{
    isDown = false;
}

void MainWidget::initializeGL()
{
    initializeOpenGLFunctions();

    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);

    skyBox.Create(":/front.jpg",":/back.jpg",":/left.jpg",":/right.jpg",":/top.jpg",":/bottom.jpg");
    tree.Create();
}

void MainWidget::resizeGL(int w, int h)
{
    float aspect = float(w) / float(h ? h : 1);
    const qreal zNear = 2.0, fov = 60.0;
    RenderCommon::Inst()->GetProjectMatrix().setToIdentity();
    RenderCommon::Inst()->GetProjectMatrix().perspective(fov, aspect, zNear, RenderCommon::Inst()->GetZFarPlane());
}

void MainWidget::paintGL()
{
    glClearColor(0,0,0,1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    skyBox.Render();
    tree.Render();
}

猜你喜欢

转载自blog.csdn.net/ZJU_fish1996/article/details/84035220