植被的渲染
植被的模型是通過建模導入得到的,本身是由多個面片組成的。
整個植被包含了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,從深度圖中讀取鼠標點擊位置的深度,通過矩陣計算還原出對應的座標。