(Vries的原教程地址如下,https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/10%20Instancing/ 关于实例化的详细设置介绍与设置与参数设置请查看这个教程,本篇旨在对Vires基于visual studio平台的编程思想与代码做Qt平台的移植,重在记录自身学习之用)
在Vires的教程中,实例化的顺序比较靠后,但这章有一部分的内容与obj模型相关且有趣,故放在“十六”。
举一个小栗子,最近在玩荒野行动时,场景建模中会有很多草,这些草只是普通的2D纹理贴图,成千上万地分布在场景中。
如果使用伪代码描述,大多数人会这样写,修改草的位置,绘图一万遍:
for(int i = 0; i != 10000; ++i){
grassShader.setQMatrix4x4(model);//在着色器中修改草的model矩阵位置
grass.draw();//绘图;
}
但整个draw()的过程,是CPU与GPU数据交互的过程,CPU指定数据缓存buffer,GPU从缓存里找到指定顶点位置,读取数据,这是一个很费时的动作,几十次还可以,如果是数十万级数的操作,无疑很消耗计算机资源。至此实例化应运而生,一次将成千上万的参数数据打包从CPU传往GPU,大大节省了计算时间。
这一张2D纹理放在100个不同的位置上,使用实例化思想,在上节“十五”代码的基础上进行修改,源代码连接在上节教程中。
顶点着色器:gl_InstanceID是一个有趣的内置变量,当决定使用实例化glDrawArraysInstanced()进行绘图时,gl_Instanced从0开始,每绘制一个实例,就+1递增,比如,在绘制第25个实例时,顶点着色器中,gl_Instanced的值为24。
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out vec3 fColor;
uniform vec2 offsets[100];
void main(){
vec2 offset = offsets[gl_InstanceID];
gl_Position = vec4(aPos + offset, 0.0f, 1.0f);
fColor = aColor;
}
片段着色器:
#version 330 core
out vec4 FragColor;
in vec3 fColor;
void main()
{
FragColor = vec4(fColor, 1.0f);
}
矩形类.cpp
#include "light.h"
Light::Light(){
core = QOpenGLContext::currentContext()->versionFunctions();
}
Light::~Light(){
core->glDeleteBuffers(1, &lightVBO);
}
void Light::init(){
float lightVertices[] = {
// 位置 // 颜色
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
-0.05f, -0.05f, 0.0f, 0.0f, 1.0f,
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
0.05f, 0.05f, 0.0f, 1.0f, 1.0f
};
core->glGenBuffers(1, &lightVBO);
core->glBindBuffer(GL_ARRAY_BUFFER, lightVBO);
core->glBufferData(GL_ARRAY_BUFFER, sizeof(lightVertices), lightVertices, GL_STATIC_DRAW);
}
void Light::drawLight(){
core->glBindBuffer(GL_ARRAY_BUFFER, lightVBO);
core->glEnableVertexAttribArray(0);
core->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
core->glEnableVertexAttribArray(1);
core->glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
core->glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100); //100表示实例化100个 矩形
}
在oglmanager类中,设置着色器中矩形要偏移的位置参数
...............
ResourceManager::loadShader("light", ":/shaders/res/shaders/light.vert", ":/shaders/res/shaders/light.frag");
QVector2D translations[100]; //vries的数据,直接拿来用
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2){
for(int x = -10; x < 10; x += 2){
QVector2D translation;
translation.setX((float)x / 10.0f + offset);
translation.setY((float)y / 10.0f + offset);
translations[index++] = translation;
}
}
for(GLuint i = 0; i < 100; i++){
QString index;
ResourceManager::getShader("light").use().setVector2f("offsets["+index.setNum(i)+"]", translations[i]);
}
..................
gl_Instanced很好用,但如果继续在顶点着色器中使用uniform作为矩形位置的参数类型,会出现uniform数值大小的限制问题,这个链接,数值限制解释了uniform类型的变量数组的最大值,我英语是一个二把刀,依稀看懂是说OpenGL3.0以上的版本,最大值至少是1024。因为实例化的数量经常成千上万,所以出现了实例化数组,即将偏移量设置为顶点属性,以解除最大值的限制。
修改顶点着色器,去除uniform offsets[100]变量,改为顶点属性:
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;
out vec3 fColor;
void main(){
gl_Position = vec4(aPos + aOffset, 0.0f, 1.0f);
fColor = aColor;
}
有了顶点属性,就要绑定buffer,将偏移量赋值,还是修改light.cpp:
#include "light.h"
#include
Light::Light(){
core = QOpenGLContext::currentContext()->versionFunctions();
}
Light::~Light(){
core->glDeleteBuffers(1, &lightVBO);
}
void Light::init(){
/************* 矩形 buffer ***************/
float lightVertices[] = {
// 位置 // 颜色
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
-0.05f, -0.05f, 0.0f, 0.0f, 1.0f,
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
0.05f, 0.05f, 0.0f, 1.0f, 1.0f
};
core->glGenBuffers(1, &lightVBO);
core->glBindBuffer(GL_ARRAY_BUFFER, lightVBO);
core->glBufferData(GL_ARRAY_BUFFER, sizeof(lightVertices), lightVertices, GL_STATIC_DRAW);
/************* 偏移量offset buffer ***************/
QVector2D translations[100];
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2){
for(int x = -10; x < 10; x += 2){
QVector2D translation;
translation.setX((float)x / 10.0f + offset);
translation.setY((float)y / 10.0f + offset);
translations[index++] = translation;
}
}
core->glGenBuffers(1, &instanceVBO);
core->glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
core->glBufferData(GL_ARRAY_BUFFER, sizeof(float)*2*100, &translations[0], GL_STATIC_DRAW);
}
void Light::drawLight(){
/************* 矩形 指定顶点属性 ***************/
core->glBindBuffer(GL_ARRAY_BUFFER, lightVBO);
core->glEnableVertexAttribArray(0);
core->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
core->glEnableVertexAttribArray(1);
core->glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
/************* 偏移量offset 指定顶点属性 ***************/
core->glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
core->glEnableVertexAttribArray(2);
core->glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
core->glVertexAttribDivisor(2, 1);
core->glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
}
这里有一个新的函数glVertexAttribDivisor(GLuint index, GLuint divisor),index表示顶点着色器中要实例化的顶点属性所在的索引,divisor为1表示每实例化一个矩形就更新顶点属性,如果是3表示每实例化3个矩形才更新,再次渲染,得到的结果还是这个画面:
当然,虽然使用了实例化数组,gl_InstanceID这个好用的变量还是可以用的,比如以下这个效果,只要稍微修改顶点着色器。
顶点着色器:
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;
out vec3 fColor;
void main(){
vec2 pos = aPos * (gl_InstanceID / 100.0);
gl_Position = vec4(pos + aOffset, 0.0f, 1.0f);
fColor = aColor;
}
一个小行星加三百个陨石,均是obj模型,顺便吐槽依据一句,五年前的Lenovo Y410P真的老了,1000个陨石跑起来有些吃力。言归正传,这是未使用“实例化”处理的300个陨石模型,使用的同一陨石模型,分布在不同的地方,绘图时,提前计算陨石的位置,for循环300次,绘出图形。
提前处理小行星的一个与陨石带的300个model矩阵,如下图所示,数据均采用Vries的原数据:
/************* 小行星 shader参数 *************/
planetModelMatrix.translate(0.0f, -3.0f, 0.0f);
planetModelMatrix.scale(4.0f);
/************* 陨石带 shader参数 *************/
rockModelsMatrix = new QMatrix4x4[ROCK_NUMBER]; //ROCK_NUM=300
GLfloat radius = 50.0;
GLfloat offset = 2.5f;
for(GLuint i = 0; i < ROCK_NUMBER; i++){
QMatrix4x4 model;
// 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]
GLfloat angle = (GLfloat)i / (GLfloat)ROCK_NUMBER * 360.0f;
GLfloat displacement = (qrand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat x = sin(angle) * radius + displacement;
displacement = (qrand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat y = displacement * 0.4f; // 让行星带的高度比x和z的宽度要小
displacement = (qrand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat z = cos(angle) * radius + displacement;
model.translate(x, y, z);
// 2. 缩放:在 0.05 和 0.25f 之间缩放
GLfloat scale = (qrand() % 20) / 100.0f + 0.05;
model.scale(scale);
// 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
GLfloat rotAngle = (qrand() % 360);
model.rotate(rotAngle, QVector3D(0.4f, 0.6f, 0.8f));
// 4. 添加到矩阵的数组中
rockModelsMatrix[i] = model;
}
在绘图函数paintGL()中,修改着色器的model矩阵,绘出图形。
/********* 绘制小行星 ************/
ResourceManager::getShader("model").use().setMatrix4f("model", planetModelMatrix);
planetModel->draw(this->isOpenLighting);
/********* 绘制陨石带 ************/
for(GLuint i = 0; i < ROCK_NUMBER; i++){
ResourceManager::getShader("model").use().setMatrix4f("model", rockModelsMatrix[i]);
rockModel->draw(this->isOpenLighting);
}
下图是采用“实例化”处理后的50000个陨石,总共近2450万个顶点,运行起来一点也不卡!摄像机的移动和视角的拖拽非常流畅,这就是“实例化”的妙用。
代码链接如下:
百度网盘链接:https://pan.baidu.com/s/1fzIZudhm3YmvW56YupJtSw 密码:assf
修改陨石带的顶点着色器,替换model矩阵。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTex;
layout (location = 2) in vec3 aNormal;
layout (location = 3) in mat4 aInstanceModelMatrix;
//uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec2 TexCoords;
out vec3 FragPos;
out vec3 Normal;
void main(){
gl_Position = projection * view * aInstanceModelMatrix * vec4(aPos, 1.0f);
FragPos = vec3(aInstanceModelMatrix * vec4(aPos, 1.0f));
Normal = mat3(transpose(inverse(aInstanceModelMatrix))) * aNormal;
TexCoords = aTex;
}