
植被的渲染
植被的模型是通过建模导入得到的,本身是由多个面片组成的。
整个植被包含了albedo贴图+法线贴图+mask贴图。
我们使用一张mask贴图来完成透明测试,丢弃额外的像素。同时,需要关闭背面剔除,避免移动视角后植被消失。


植被的动画
接下来给植被加入一些随风轻微晃动的效果。此处的计算比较简单,就是使其沿着法线方向和垂直法线的方向做正弦周期摆动。为了避免整个植被都在晃动,有一种“站不稳”的感觉,此处设定了晃动幅度与y轴坐标相关联,也就是越高的地方,晃动幅度越大。
为了避免每个植被晃动一致,我们可以加入一些随机参数。此处使用worldPos.x变量保证每个植被得晃动周期能够错开。
此处的计算放在GPU中完成,在顶点着色器中实现,大致的计算如下:
float y = 100 * localPos.y;
y = y / (1 + y);
y = max(0, (y - 0.5) / 20);
float ratio = fWindSpeed * time;
vec3 dir1 = v_normal;
vec3 dir2 = cross(v_normal,vec3(0, 1, 0));
float strength = sin(ratio + worldPos.x) * y;
vec3 offset1 = dir1 * strength;
vec3 offset2 = dir2 * strength;
vec3 offset = mix(a1, a2, 0.5);
worldPos = worldPos + offset;
实例化渲染
渲染大量的植被会给GPU带来一定的负担。像这样大量的、较为一致的物体,可以使用实例化渲染进行优化。
在OpenGL中,我们使用glDrawElementsInstanced来完成实例化渲染。
由于每株植被具有一些特殊的信息,比如它们的位置、大小可能是不一致的,我们可以把这些额外的数据绑定到顶点上,一并传递给GPU。
此处,可以定义一个结构体存储这些特殊信息:
struct SInstanceParam
{
QVector3D m_pos;
QVector3D m_scale;
SInstanceParam() { }
SInstanceParam(const QVector3D& pos)
:m_pos(pos), m_scale(QVector3D(1,1,1)) { }
};
除了额外的顶点数据,我们本身还有一个结构体,存储的是基本的顶点信息:
struct VertexData
{
QVector3D position;
QVector3D tangent;
QVector3D normal;
QVector2D texcoord;
};
对于每个Mesh而言,我们使用一个MeshBuffer类来记录它们的buffer数据,里面目前包含了arrayBuf(记录基本顶点信息),indexBuf(记录顶点的索引关系,即哪几个点构成一个三角形),以及我们新加入的instanceBuf(记录实例化渲染中,额外的一些顶点数据)。也包括了实例化数据的初始化、重新分配以及更新的方法。
struct MeshBuffer
{
QOpenGLBuffer arrayBuf;
QOpenGLBuffer indexBuf;
QOpenGLBuffer instanceBuf;
int vertexNum = 0;
int indiceNum = 0;
int instanceNum = 1;
MeshBuffer() : indexBuf(QOpenGLBuffer::IndexBuffer)
{
arrayBuf.create();
indexBuf.create();
instanceBuf.create();
}
~MeshBuffer()
{
arrayBuf.destroy();
indexBuf.destroy();
instanceBuf.destroy();
}
bool IsInit()
{
return vertexNum && indiceNum;
}
void Init(VertexData* vertex, int num)
{
vertexNum = num;
arrayBuf.bind();
arrayBuf.allocate(vertex, vertexNum * static_cast<int>(sizeof(VertexData)));
}
void Init(TerrainVertexData* vertex, int num)
{
vertexNum = num;
arrayBuf.bind();
arrayBuf.allocate(vertex, vertexNum * static_cast<int>(sizeof(TerrainVertexData)));
}
void Init(SInstanceParam* data, int num)
{
instanceNum = num;
instanceBuf.bind();
instanceBuf.allocate(data, num * static_cast<int>(sizeof(SInstanceParam)));
}
void Init(GLushort* indice, int num)
{
indiceNum = num;
indexBuf.bind();
indexBuf.allocate(indice, indiceNum * static_cast<int>(sizeof(GLushort)));
}
void bind()
{
arrayBuf.bind();
indexBuf.bind();
}
void bindInstance()
{
instanceBuf.bind();
}
void realloc(VertexData* vertex, int num, int offset = 0)
{
arrayBuf.bind();
arrayBuf.write(offset, vertex, num * static_cast<int>(sizeof(VertexData)));
}
void realloc(TerrainVertexData* vertex, int num, int offset = 0)
{
arrayBuf.bind();
arrayBuf.write(offset, vertex, num * static_cast<int>(sizeof(TerrainVertexData)));
}
void realloc(GLushort* indice, int num, int offset = 0)
{
indexBuf.bind();
indexBuf.write(offset, indice, num * static_cast<int>(sizeof(GLushort)));
}
void realloc(SInstanceParam* data, int num)
{
instanceNum = num;
instanceBuf.destroy();
instanceBuf.create();
instanceBuf.bind();
instanceBuf.allocate(data, num * static_cast<int>(sizeof(SInstanceParam)));
}
GLuint updateInstance(QString name, int offset, int size, int totalSize, QOpenGLShaderProgram* program, QOpenGLExtraFunctions* gl, bool bFinish)
{
instanceBuf.bind();
GLuint location = static_cast<GLuint>(program->attributeLocation(name));
gl->glEnableVertexAttribArray(location);
gl->glVertexAttribPointer(location, size, GL_FLOAT, GL_FALSE, totalSize, (void*)offset);
gl->glVertexAttribDivisor(location,1);
return location;
}
};
我们定义一个模型(Model) 是多个Mesh的组合,每个对象包含了一个模型,每次点击地面新增植被的时候,我们更新对应的实例数据,这些数据以数组的形式存在。
也就是说,如果我们有n个不同类型的植被,我们将创建n个模型。每个模型包含了该类型植被的多个实例,这些实例的数据都存储在MeshBuffer的instanceBuf中。
此处定义instanceNum,大于等于0时才开启实例化渲染,否则默认正常渲染。
class Model
{
private:
int instanceNum = -1;
vector<Mesh*> vecMesh;
public:
// 每次鼠标点击地面的时候,调用一下这个函数,更新实例化数据
void SetInstanceData(vector<SInstanceParam>& data)
{
int num = static_cast<int>(data.size());
instanceNum = num;
for(auto& mesh : vecMesh)
{
mesh->buffer->realloc(data.data(), num);
}
}
// ...
}
接下来是渲染部分,我们传入基本的顶点数据后,绑定实例参数的buffer,并传入数据。
void GeometryEngine::drawObj(MeshBuffer* meshBuffer, QOpenGLShaderProgram* program, bool bTess)
{
meshBuffer->bind();
auto gl = QOpenGLContext::currentContext()->extraFunctions();
int offset = 0;
int vertexLocation = program->attributeLocation("a_position");
program->enableAttributeArray(vertexLocation);
program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData));
offset += sizeof(QVector3D);
int tangentLocation = program->attributeLocation("a_tangent");
program->enableAttributeArray(tangentLocation);
program->setAttributeBuffer(tangentLocation, GL_FLOAT, offset, 3, sizeof(VertexData));
offset += sizeof(QVector3D);
int normalLocation = program->attributeLocation("a_normal");
program->enableAttributeArray(normalLocation);
program->setAttributeBuffer(normalLocation, GL_FLOAT, offset, 3, sizeof(VertexData));
offset += sizeof(QVector3D);
int texcoordLocation = program->attributeLocation("a_texcoord");
program->enableAttributeArray(texcoordLocation);
program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData));
offset = 0;
if(meshBuffer->instanceNum >= 1)
{
GLuint loc1 = meshBuffer->updateInstance("a_offset", offset, 3, sizeof(SInstanceParam), program, gl, false);
offset += sizeof(QVector3D);
GLuint loc2 = meshBuffer->updateInstance("a_scale", offset, 3, sizeof(SInstanceParam), program, gl, true);
gl->glDrawElementsInstanced(GL_TRIANGLES, meshBuffer->indiceNum, GL_UNSIGNED_SHORT, nullptr, meshBuffer->instanceNum);
gl->glVertexAttribDivisor(loc1, 0);
gl->glVertexAttribDivisor(loc2, 0);
}
else if(meshBuffer->instanceNum == -1)
{
// ...
// 此处为正常渲染模块
}
}
在对应的着色器中,我们可以像使用普通顶点数据一样,使用传入的实例化数据,并且能确保每个植被都有自己独立的变量:
in vec3 a_offset;
in vec3 a_scale;
点选创建效果
图中实现了点击地面后创建植被的效果,通过鼠标点选地面计算对应世界坐标的方法已在地形画刷一文中介绍过。此处使用的是记录相机空间的深度信息,然后调用OpenGL的内置API,从深度图中读取鼠标点击位置的深度,通过矩阵计算还原出对应的坐标。