使用Qt3D 2.0的Billboarding

时间:2017-10-18 23:49:56

标签: c++ 3d qml qt5 qt3d

我正在寻找在Qt3D中创建广告牌的最佳方法。我想要一架面向相机的飞机,无论它在哪里,当相机向前或向后移动时都不会改变尺寸。我已经阅读了如何使用GLSL顶点和几何着色器进行此操作,但我正在寻找Qt3D方式,除非客户着色器是最有效和最好的广告牌方式。

我看过了,似乎我可以通过属性在QTransform上设置矩阵,但是我不清楚如何操作矩阵,或者有更好的方法?我正在使用C ++ api,但QML答案可以。我可以把它移植到C ++。

1 个答案:

答案 0 :(得分:6)

如果您只想绘制一个广告牌,您可以添加一个平面并在相机移动时旋转它。但是,如果您希望有效地使用数千或数百万个广告牌,我建议使用自定义着色器。我们这样做是为了在Qt3D中绘制冒名顶球。

但是,我们没有使用几何着色器,因为我们的目标是不支持几何着色器的系统。相反,我们通过在原点中放置四个顶点并在着色器上移动它们来仅使用顶点着色器。为了创建许多副本,我们使用了实例化绘图。我们根据球体的位置移动了每组四个顶点。最后,我们移动每个球体的四个顶点中的每一个,使得它们产生总是面向相机的广告牌。

从子类化QGeometry开始,创建一个缓冲函子,创建四个点,全部在原点(参见spherespointgeometry.cpp)。给每个点一个我们以后可以使用的ID。如果使用几何着色器,则不需要ID,只能创建一个顶点。

class SpheresPointVertexDataFunctor : public Qt3DRender::QBufferDataGenerator
{
public:
    SpheresPointVertexDataFunctor()
    {
    }

    QByteArray operator ()() Q_DECL_OVERRIDE
    {
        const int verticesCount = 4;
        // vec3 pos
        const quint32 vertexSize = (3+1) * sizeof(float);

        QByteArray verticesData;
        verticesData.resize(vertexSize*verticesCount);
        float *verticesPtr = reinterpret_cast<float*>(verticesData.data());

        // Vertex 1
        *verticesPtr++ = 0.0;
        *verticesPtr++ = 0.0;
        *verticesPtr++ = 0.0;
        // VertexID 1
        *verticesPtr++ = 0.0;

        // Vertex 2
        *verticesPtr++ = 0.0;
        *verticesPtr++ = 0.0;
        *verticesPtr++ = 0.0;
        // VertexID 2
        *verticesPtr++ = 1.0;

        // Vertex 3
        *verticesPtr++ = 0.0;
        *verticesPtr++ = 0.0;
        *verticesPtr++ = 0.0;
        // VertexID3
        *verticesPtr++ = 2.0;

        // Vertex 4
        *verticesPtr++ = 0.0;
        *verticesPtr++ = 0.0;
        *verticesPtr++ = 0.0;
        // VertexID 4
        *verticesPtr++ = 3.0;

        return verticesData;
    }

    bool operator ==(const QBufferDataGenerator &other) const Q_DECL_OVERRIDE
    {
        Q_UNUSED(other);
        return true;
    }

    QT3D_FUNCTOR(SpheresPointVertexDataFunctor)
};

对于真实位置,我们使用了单独的QBuffer。我们还设置了颜色和比例,但我在这里省略了(见spheredata.cpp):

void SphereData::setPositions(QVector<QVector3D> positions, QVector3D color, float scale)
{
    QByteArray ba;
    ba.resize(positions.size() * sizeof(QVector3D));
    SphereVBOData *vboData = reinterpret_cast<QVector3D *>(ba.data());
    for(int i=0; i<positions.size(); i++) {
        QVector3D &position = vboData[i];
        position = positions[i];
    }
    m_buffer->setData(ba);
    m_count = positions.count();
}

然后,在QML中,我们将几何与QGeometryRenderer中的缓冲区相连。如果您愿意,这也可以用C ++完成(参见 Spheres.qml):

GeometryRenderer {
    id: spheresMeshInstanced
    primitiveType: GeometryRenderer.TriangleStrip
    enabled: instanceCount != 0
    instanceCount: sphereData.count

    geometry: SpheresPointGeometry {
        attributes: [
            Attribute {
                name: "pos"
                attributeType: Attribute.VertexAttribute
                vertexBaseType: Attribute.Float
                vertexSize: 3
                byteOffset: 0
                byteStride: (3 + 3 + 1) * 4
                divisor: 1
                buffer: sphereData ? sphereData.buffer : null
            }
        ]
    }
}

最后,我们创建了自定义着色器来绘制广告牌。请注意,因为我们正在绘制冒名顶替球体,所以增加了广告牌的大小,以便从尴尬的角度处理片段着色器中的光线追踪。您可能一般不需要2.0*0.6因子。

顶点着色器:

#version 330

in vec3 vertexPosition;
in float vertexId;
in vec3 pos;
in vec3 col;
in float scale;

uniform vec3 eyePosition = vec3(0.0, 0.0, 0.0);

uniform mat4 modelMatrix;
uniform mat4 mvp;

out vec3 modelSpherePosition;
out vec3 modelPosition;
out vec3 color;
out vec2 planePosition;
out float radius;
vec3 makePerpendicular(vec3 v) {
    if(v.x == 0.0 && v.y == 0.0) {
        if(v.z == 0.0) {
            return vec3(0.0, 0.0, 0.0);
        }
        return vec3(0.0, 1.0, 0.0);
    }
    return vec3(-v.y, v.x, 0.0);
}

void main() {
    vec3 position = vertexPosition + pos;
    color = col;
    radius = scale;
    modelSpherePosition = (modelMatrix * vec4(position, 1.0)).xyz;

    vec3 view = normalize(position - eyePosition);
    vec3 right = normalize(makePerpendicular(view));
    vec3 up = cross(right, view);

    float texCoordX = 1.0 - 2.0*(float(vertexId==0.0) + float(vertexId==2.0));
    float texCoordY = 1.0 - 2.0*(float(vertexId==0.0) + float(vertexId==1.0));
    planePosition = vec2(texCoordX, texCoordY);

    position += 2*0.6*(-up - right)*(scale*float(vertexId==0.0));
    position += 2*0.6*(-up + right)*(scale*float(vertexId==1.0));
    position += 2*0.6*(up - right)*(scale*float(vertexId==2.0));
    position += 2*0.6*(up + right)*(scale*float(vertexId==3.0));

    vec4 modelPositionTmp = modelMatrix * vec4(position, 1.0);
    modelPosition = modelPositionTmp.xyz;

    gl_Position = mvp*vec4(position, 1.0);
}

片段着色器:

#version 330

in vec3 modelPosition;
in vec3 modelSpherePosition;
in vec3 color;
in vec2 planePosition;
in float radius;

out vec4 fragColor;

uniform mat4 modelView;
uniform mat4 inverseModelView;
uniform mat4 inverseViewMatrix;
uniform vec3 eyePosition;
uniform vec3 viewVector;

void main(void) {
    vec3 rayDirection = eyePosition - modelPosition;
    vec3 rayOrigin = modelPosition - modelSpherePosition;

    vec3 E = rayOrigin;
    vec3 D = rayDirection;

    // Sphere equation
    //      x^2 + y^2 + z^2 = r^2
    // Ray equation is
    //     P(t) = E + t*D
    // We substitute ray into sphere equation to get
    //     (Ex + Dx * t)^2 + (Ey + Dy * t)^2 + (Ez + Dz * t)^2 = r^2
    float r2 = radius*radius;
    float a = D.x*D.x + D.y*D.y + D.z*D.z;
    float b = 2.0*E.x*D.x + 2.0*E.y*D.y + 2.0*E.z*D.z;
    float c = E.x*E.x + E.y*E.y + E.z*E.z - r2;

    // discriminant of sphere equation
    float d = b*b - 4.0*a*c;
    if(d < 0.0) {
        discard;
    }

    float t = (-b + sqrt(d))/(2.0*a);
    vec3 sphereIntersection = rayOrigin + t * rayDirection;

    vec3 normal = normalize(sphereIntersection);
    vec3 normalDotCamera = color*dot(normal, normalize(rayDirection));

    float pi = 3.1415926535897932384626433832795;

    vec3 position = modelSpherePosition + sphereIntersection;

    // flat red
    fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

自从我们第一次实施此功能以来已经有一段时间了,现在可能有更简单的方法,但这可以让您了解所需的部分。