LearnGL - 18 - Instancing/Instanced Rendering - 多实例渲染1 - glDrawElementsInstanced


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 < primCount; ++i) {
		glDrawElements(mode, count, type, indices);
	}
}

实践


应用层

	void MyTesting_Instancing_Rock::init() {
		Component::init();
		........
		mat->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 < INSTANCING_COUNT; i++) {
			mat4 tMat, rMat, sMat, com_rMat;
			tMat = glm::identity<mat4>();
			rMat = glm::identity<mat4>();
			sMat = glm::identity<mat4>();
			com_rMat = glm::identity<mat4>();

			// s
			vec3 s = vec3(
				loc_scl.x + ran_range(0.0f, -ran_init_scale.x),
				loc_scl.y + ran_range(0.0f, -ran_init_scale.y),
				loc_scl.z + ran_range(0.0f, -ran_init_scale.z));
			// r
			vec3 r = vec3(
				ran_range(-360.0f, 360.0f),
				ran_range(-360.0f, 360.0f),
				ran_range(-360.0f, 360.0f));
			// t
			vec3 t = vec3(ran_range(80.0f, 100.0f), ran_range(-1.0f, 1.0f), 0.0f);

			// s
			sMat = glm::scale(sMat, s);
			// r
			rMat = glm::rotate(rMat, D2R(r.y), vec3(0, 1, 0));
			rMat = glm::rotate(rMat, D2R(r.x), vec3(1, 0, 0));
			rMat = glm::rotate(rMat, D2R(r.z), vec3(0, 0, 1));
			// t
			tMat = glm::translate(tMat, t);

			// 公转角度
			com_rMat = glm::rotate(com_rMat, D2R(ran_range(-360.0f, 360.f)), vec3(0, 1, 0));

			// trs 矩阵
			instancingMat[i] = (com_rMat * tMat * rMat * sMat);
		}
	}
	void MyTesting_Instancing_Rock::update() {
		Component::update();

		GameObject* go = getOwner();
		go->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
}

绘制效果

在这里插入图片描述

在这里插入图片描述


draw call

使用两个 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
		<< "Maximun number of Uniform Block Size : "
		<< maxUniformBlockSize
		<< std::endl;

输出:Maximun number of Uniform Block Size : 65536

注意不同的硬件中,这个限制大小是不同的,这里是我的笔记本上的测试结果

这个:65536 大小的单位是:字节

uniform Instancing_UBO {
	mat4 instancingMat[1000];		// instancing 矩阵
};

从上面 shader 代码可以看到 UBO 中有一个 mat4 的数组成员,数组大小为1000,一个 mat4 就是相当于 4个 vec4

每个 vec4 也就是 4个 float

所以一个 mat4 相当于 float4x4(DX的 shader : HLSL 数据类型),也就是 4x4=16 个 float

一个 float = 4 bytes,所以 mat4 = 16 * 4 个 bytes = 64 个 bytes

那么 1000 个 mat4 就是 1000 * 64 = 64000 bytes ,已经相当接近 65536 了,

那么我们可以通过 MaxUBOSize : 65536 除以单个 mat4 需要的字节(64字节),就可以知道我们的硬件最大可以给单个 UBO 分配多大的大小:

65536 / 64 = 1024,所以我的硬件是只支持单个 UBO 最多 1024个 mat4 的大小,也就是 64 KB 的大小

OK,我测试后,1024 个实例化是没有 shader 编译错误问题的

那么为了测试,我改为 1025 个后,也出现了上面的大小限制的问题了


上面的示例中,只是在 UBO 中使用了一个 mat4 的属性,如果还有其他的颜色,或是其他的属性,那么 UBO 的大小限制很快就成为最大的问题的

那么 UBO 的方式限制这么大,我们在一些场景中的绘制实例数量会大大超过这个数量,有些甚至到达 10万个,或以上

除了 UBO 大小之外,不知道大家没有发现,这种方式,还需要对 glsl Shader 层的 UBO指定固定大小

使用起来很不方便,这个数量很大时候取决于应用层的实例数量而动态变化大小的,虽然可以将 UBO 设置大些,但 UBO 大小本身有限制,而且不同硬件限制大小还不一样,如,我这个笔记本,1024 个 mat4 的大小(64KB)就到顶了

所以使用 UBO 的方式限制问题还是很大的,后续的文章会使用更好的方式来实现


References

  • OpenGL 红宝书 第9版 第三章 - 多实例渲染
  • 实例化
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值