- Instancing - 多实例渲染
- Draw Instancing API - 绘制的接口
- 实践
- 应用层
- Vertex Shader
- 绘制效果
- draw call
- 问题
- Uniform block size 的大小限制
- 获取 单个UBO大小 的最大限制
- References
LearnGL - 学习笔记目录
Instancing - 多实例渲染引用 OpenGL 红宝书 - 第9版中的部分描述:
实例化(instancing)或者多实例渲染(instanced rendering)是一种连续执行多条相同的渲染命令的方法,并且每个渲染命令所产生的结果都会有轻微的差异。这是一种非常有效的,使用少量 API 调用来渲染大量几何体的方法。OpenGL 中已经提供了一些常用绘制函数的多变量形式来优化命令的多次执行。此外。OpenGL 中也提供了多种机制,允许着色器使用绘制的不同实例作为输入。并且对每个实例(而不是每个顶点)都赋予不同的顶点属性值。
这种渲染方法通常是对很多网格相同的对象要渲染时,对局部不同的属性,如:transform变换(mat4 modelMatrix[n])、颜色(vec4 color[n])、纹理(sampler2DArray),等,其他的属性都可以使用数组形式来存放(也不一定是数组,后面可以是多实例的顶点属性的方式,不过这需要依赖硬件的支持,不过现在的硬件,基本都会支持了,除非比较低端的嵌入式,如:低端手机),让后使用着色器中的 gl_InstanceID
来作为索引值获取对应数组中的属性值。
gl_InstanceID 每绘制一个实例后的实例数量都会+1
Draw Instancing API - 绘制的接口接口有好几种,而不同的接口,实现上也稍有不同:
- glDrawArrayInstanced
- glDrawElementsInstanced
- glDrawElementsInstancedBaseVertex
- glVertexAttribDivisor
- glDrawArrayInstancedBaseInstance
- glDrawElementInstancedBaseInstance
- glDrawElementsInstanceBaseVertexBaseInstance
这里 的测试方法使用的是:glDrawElementsInstanced 接口,它对我们之前的文章中使用的 glDrawElements 非常类似,只不过 glDrawElementsInstanced 多一个实例化数量的参数,所以使用起来也是非常简单的
void glDrawElementsInstanced(
GLenum mode,
GLsizei count,
GLenum type,
const void* indices,
GLsizei primCount);
通过 mode、count 和 indices 所构成的几何体图元集(相当于 glDrawElements() 函数所需的独立参数),绘制它的 primCount 个实例。与 glDrawArrayInstanced() 类似,对于每个实例,内置变量 gl_InstanceID 都会一次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。
再次注意到,glDrawElementsInstanced() 的参数与 glDrawElements() 的是等价的,只是新增了 primCount 参数。每次调用多实例函数时,在本质上 OpenGL 都会根据 primCount 参数来设置多次运行整个命令。这看起来并不是很有用的功能。不过,OpenGL 提供了两种机制来设置对应不同实例的顶点属性,并且在顶点着色器中可以获取当前实例所对应的索引号。
它的底层绘制伪代码就像是:
void glDrawElementsInstanced(
GLenum mode,
GLsizei count,
GLenum type,
const void* indices,
GLsizei primCount) {
for (int i = 0; i instancing = true;
mat->instanc_count = INSTANCING_COUNT;
// mesh renderer - component
MeshRenderer* mr = new MeshRenderer();
mr->setQueue(RenderQueueType::Transprent);
getOwner()->addComp(mr);
UBO_DATA* ubo_data = new UBO_DATA("Instancing_UBO");
ubo_data->addNameMap({ "instancingMat", (size_t)instancingMat });
ShaderProgram::registerUBO(ubo_data);
GameObject* go = getOwner();
vec3 loc_scl = go->getTrans()->local_scale;
for (size_t i = 0; i getTrans()->local_rotation.y += (double)Timer::inst()->deltaTime * sp_rotate;
}
这里使用了:UBO (uniform block object) 的方式,但这种方式不是最高效的,如果使用的是 unfirom 的话,同样有大小,与数量的限制
UBO_DATA* ubo_data = new UBO_DATA("Instancing_UBO");
ubo_data->addNameMap({ "instancingMat", (size_t)instancingMat });
ShaderProgram::registerUBO(ubo_data);
开启 instancing 的方式,这种方式比较简单,对材质设置好属性即可,在上面的代码中就是:
mat->instancing = true;
mat->instanc_count = INSTANCING_COUNT;
在绘制接口的时候,我们会根据是否开启了 instancing 来做分支处理:
if (mat->instancing) {
// 实例化绘制图元
glDrawElementsInstanced(
DrawState_Pritmive::vec_value[(int)mesh->primiveType],
mesh->indices_byte_size(), GL_UNSIGNED_INT, (GLvoid*)(0), mat->instanc_count);
checkGLError();
}
else {
// 绘制图元,可以按网格给定的图元类型来绘制
glDrawElements(
DrawState_Pritmive::vec_value[(int)mesh->primiveType],
mesh->indices_byte_size(), GL_UNSIGNED_INT, (GLvoid*)(0));
checkGLError();
}
Vertex Shader
主要是 vertex shader 有变化,其他的 shader 没有任何变化
// jave.lin - testing_instancing.vert
#version 450 compatibility
#extension GL_ARB_shading_language_include : require
#include "/Include/my_global.glsl"
// vertex data
in vec3 vPos; // 顶点坐标
in vec2 vUV0; // 顶点纹理坐标
in vec3 vNormal; // 顶点法线
// vertex data - interpolation
out vec2 fUV0; // 给 fragment shader 传入的插值
out vec3 fNormal; // 世界坐标顶点法线
out vec3 fWorldPos; // 世界坐标
uniform Instancing_UBO {
mat4 instancingMat[1000]; // instancing 矩阵
};
void main() {
mat4 instancing_mMat = instancingMat[gl_InstanceID]; // 从 ubo 中获取 model matrix
mat4 new_mMat = mMat * instancing_mMat; // 将原来的 mMat 累计变换到新的 model matrix
mat4 it_mMat = transpose(inverse(new_mMat)); // 求得新的逆矩阵的转置矩阵,用于变换法线用
// vec4 worldPos = mMat * vec4(vPos, 1.0); // 原来直接 model matrix 变换即可
vec4 worldPos = new_mMat * vec4(vPos, 1.0); // 现在使用新的 model matrix 变换
fUV0 = vUV0; // UV0
fNormal = normalize(mat3(it_mMat) * vNormal); // 用新的 it_mMat 矩阵来将模型空间的法线,变换到,世界坐标法线
fWorldPos = worldPos.xyz; // 世界坐标
gl_Position = vpMat * worldPos; // Clip pos
}
绘制效果
使用两个 DC (draw call),却绘制了 1001 个对象,1个行星,1000个陨石
前面有说,这种方法使用的是:glDrawElementsInstanced + UBO内的数组方法
这种方法的 UBO 对象大小限制比较大,后面会只用其他接口来实现,会更好
Uniform block size 的大小限制如,上面的示例中,我们绘制了1000个陨石,如果我们将数量提升到 2000 个,会怎么样?
在编译 shader 的时候就会报错:
那要怎么样才能知道 UBO 最大的大小限制呢?
获取 单个UBO大小 的最大限制使用 glGetIntegerv 接口即可,传入:GL_MAX_UNIFORM_BLOCK_SIZE
// GL_MAX_UNIFORM_BLOCK_SIZE
GLint maxUniformBlockSize;
glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize);
std::cout
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?