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();
}