公告牌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;
};