demo演示
![](https://img-blog.csdnimg.cn/direct/8e5a9238f72f4b4aa3c4f77dbd7b6778.png)
![](https://img-blog.csdnimg.cn/direct/046684470151470b99d71f83d4b744ed.png)
实例化 VS 批处理
理解一次绘制的顺序(每帧):
- 顶点数据布局,所有顶点分组(分为一个个顶点,每个顶点包含顶点属性)
- 绑定着色器,表明接下来要使用哪个着色器program
- uniform / uniform buffer传入glsl中,配置好glsl所有数据
- 调用DrawCall()时,绑定VAO,根据在VAO查找每个顶点offest 以及 每个顶点属性offest,每个顶点调用一次绑定的着色器program,所有顶点依次渲染
理解多次绘制的顺序(每帧):
- 首先,为什么大量绘制物体,要调用这么多次DrawCall()?
- 因为我们定义的顶点数组只是一个数组,每次调用DrawCall(),仅绘制了这个数组中的所有顶点,若想大量绘制物体,需要多次调用。
- 过程:
- …… 34步:for(){……DrawCall()}
- for中每次调用DrawCall()时,都会重新进行glsl配置(非必须如果想位置相同的话,完全一致),绑定VAO,顶点依次渲染
非实例化渲染函数缺点:
- CPU -> GPU的通信:
- 当每次将VAO中的数据,传输到glsl(GPU),都要告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性……)
- 比如绘制大量同样图元,传入相同的顶点数据,每次DrawCall(),都要重新 通信/查找
- 注意:
- 影响性能的非渲染,GPU并行渲染计算速度非常快,GPU一直在等待CPU
- 当所有图元的顶点数据都一致,可以用实例化渲染函数(),仅调用一次DrawCall Instanced(),需要一个额外的参数,叫做实例数量(Instance Count)
- 本质:
- 原来的 DrawCall()仅可以绘制一组顶点数据,这回调用一次渲染函数,可以绘制指定数量的(一组顶点数据)
- 但是目前绘制都是相同的,也就是位置一致,如何将每个实体放到不同的位置?
- 利用内建变量:使用实例化渲染调用时,GLSL在顶点着色器中嵌入了另一个内建变量,gl_InstanceID。gl_InstanceID会从0开始,在每个实例被渲染时递增1
- 利用uniform【】,传入每个图元偏移量offest,对于同一个gl_InstanceID图元,的所有顶点的offest都是相同的
-
但是:
- uniform数据大小有上限:当绘制大量实体时,代替方案是实例化数组
- 首先我们将offest定义为一个顶点属性(能够让我们储存更多的数据),
- 新建VBO存储到,AttribPointer其中一个属性指针上
- glVertexAttribDivisor(顶点属性,属性除数)
- 属性除数默认为0,表示每几个实例更新顶点属性。
批处理:看看Hazel引擎架构如何进行批处理的
- 首先什么是批处理?核心思想是将多个图元组合成一个大的图元进行绘制,从而减少OpenGL的绘图调用次数,以及状态的切换
- 也就是init->drawAPI->renderer,非交错布局 111->222->333
- 绘制不同实体,都要经历下面的过程
- 建立变量:struct表示实体顶点属性,数组【struct】每个顶点的属性(每个顶点struct都会添加到数组),ptr指针负责记录所有顶点添加完成后,最后ptr更新到的位置,int IndexCount记录所有顶点数目。
- init():对不同的VAO(VBO,EBO,layout),shader,初始化,和我们正常绘制init是一样的
- 调用Draw():传入每个顶点属性,添加到 数组【struct】,ptr后移,顶点个数+=;
- renderer:
- 通过glBufferSubData()一点点填充buffer,其中offest设置为0,覆盖原有的数据(不用担心这一次顶点数量变少,有ptr重新更新,会记录本次的位置)
- ptr- 数组起点 = 当前顶点数据(非索引数,顶点个数 !=索引个数)
- bind()需要的各种状态
- glDrawElements()绘制所有顶点数据
静态/离线合批 batching VS 动态/实时合批 batching VS 实例化instancing:参考
- 合批和实例化是两种强大的渲染优化技术,获得最佳的渲染性能,
- 减少DrawCall(是指CPU向GPU发送的一次绘制命令,cpu会将顶点数据传入显存,之后由显卡进行绘制,,以及减少GPU切换渲染状态的次数
- 合批:
- 显著减少Draw Call的数量
- 静态合批:在非运行时间进行,提前准备好合并后的网格并存储在文件中,Mesh合并不占用运行时效率,但它会占用一定的CPU和带宽资源
- 运行时顶点、索引信息也不会发生变化,所以无需CPU消耗算力维护,减少了DrawCall()的次数
- 引擎中:静态本质就是对标记为static的Mesh自动合并
- 动态合批:在游戏运行时相关资源做合批处理,每一帧都进行一遍的Mesh合并,用复制数据的性能消耗换取提交Drawcall()的性能消耗,需要消耗CPU资源进行实时计算和带宽资源
- 实例化:
- 只提交一个模型网格让GPU绘制很多个地方,通知显卡在绘制这些顶点时,重复绘制的次数。
- 重复绘制大量相同或相似物体的场景
- 静态合批 > Instancing > 动态合批(负优化)
unsigned int amount = 1000;//总数
glm::mat4 *modelMatrices;
modelMatrices = new glm::mat4[amount];//变换矩阵数组
srand(glfwGetTime()); // 初始化随机种子
float radius = 50.0;//半径
float offset = 2.5f;//偏移量
for(unsigned int i = 0; i < amount; i++)//求每个随机的变换矩阵
{
glm::mat4 model;
//位移:求xyz,偏移的范围是 [-offset, offset]
float angle = (float)i / (float)amount * 360.0f;//角度,将360分成实体个数份,【0——360】°
float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;//(rand % 500)约束到0--499,约等/100(0--5),-offset[-2.5-2.5]
float x = sin(angle) * radius + displacement;//angle(0,90,180,360,0)->sin(angle)(0,1,0,-1,0) * 50(0,50,0,-50,0),+随机[-2.5-2.5]
displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;//rand会更新
float y = displacement * 0.4f; // [-2.5-2.5] * 0.4
displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
float z = cos(angle) * radius + displacement;angle(0,90,180,360,0)->cos(1,0,-1,0,1),也就是从+z开始,逆时针的矩阵
model = glm::translate(model, glm::vec3(x, y, z));//获得随机位置
//缩放:
float scale = (rand() % 20) / 100.0f + 0.05;
model = glm::scale(model, glm::vec3(scale));
//旋转:
float rotAngle = (rand() % 360);
model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
//添加
modelMatrices[i] = model;
}
绘 制大量实例:
- 创建glm::mat4变换数组【】(相对于圆环),存储每个实体的变换(随机旋转,缩放,偏移)
- 对每个model 的meshes的VAO layout