学习OpenGL ES for Android(二十五)— 实例化

简介

本章对应文档
如果我们想要绘制许多相同的物体,只是他们的位置或大小不同,按照之前学习的知识,正常的做法是构建一个变化矩阵数组,通过for循环进行循环绘制,当我们绘制的物体数量逐渐增加时会发现设备越来越卡。如果我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,就会更方便了。这就是实例化(Instancing)

实例化

实例化只能在OpenGL ES 3.0之后才能使用,实例化会使用到这些方法

glDrawArraysInstanced( int mode, int first, int count, int instanceCount ):前三个参数和glDrawArrays( int mode, int first, int count )方法一样,最后一个instanceCount参数表示要“重复”绘制多少次。

glDrawElementsInstanced( int mode, int count, int type, java.nio.Buffer indices, int instanceCount ):前四个参数和glDrawElements( int mode, int count, int type, java.nio.Buffer indices )方法一样,最后一个instanceCount参数表示要“重复”绘制多少次。

这两个函数本身并没有什么用。渲染同一个物体一千次对我们并没有什么用处,每个物体都是完全相同的,而且还在同一个位置。还需要配合内建变量:gl_InstanceID。使用实例化渲染调用时,gl_InstanceID会从0开始,在每个实例被渲染时递增1。比如说,我们正在渲染第43个实例,那么顶点着色器中它的gl_InstanceID将会是42。因为每个实例都有唯一的ID,我们可以建立一个数组,将ID与位置值对应起来,将每个实例放置在世界的不同位置。
最后是传入我们的数据,顶点,颜色等数据和之前没有变化,主要是变化矩阵数据的接收。

使用实例化绘制矩形

以绘制多个矩形为例,第一种方式,生成多个偏移量数据,然后使用for循环传入数据,在顶点着色器中使用数组接收并根据gl_InstanceID获取当前的数据再进行绘制。顶点着色器的代码如下,

#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in vec3 aColor;

out vec3 fColor;

uniform vec2 offsets[100];
void main(){
	vec2 offset = offsets[gl_InstanceID];
	gl_Position = vec4(aPosition + offset, 0.0, 1.0);
    fColor = aColor;
}

片段着色器就是简单绘制,代码不再赘述,生成和传入数据的方式如下

	……
	translationArray = new float[100][2];
	int index = 0;
	float offset = 0.1f;
	for (int y = -10; y < 10; y += 2) {
    
    
		for (int x = -10; x < 10; x += 2) {
    
    
			float[] translation = new float[2];
			translation[0] = (float) x / 10.0f + offset;
			translation[1] = (float) y / 10.0f + offset;
			translationArray[index++] = translation;
		}
	}
	……
	for (int i = 0; i < 100; i++) {
    
    
		GLES20.glUniform2fv(GLES20.glGetUniformLocation(quadsRenderer.shaderProgram, "offsets[" + i + "]"), 2, OpenGLUtil.createFloatBuffer(translationArray[i]));
	}
	GLES30.glDrawArraysInstanced(GLES20.GL_TRIANGLES, 0, 6, 100);

效果图如下,在屏幕上会生成100个矩形,
实例化

使用实例化数组

虽然上面的代码可以实现我们的效果,但是当我们需要绘制的物体更多,从而超过最大能够发送至着色器的uniform数据大小时,则需要使用实例化数组的方式,我们定义一个顶点属性来接收,仅在顶点着色器渲染一个新的实例时数据才会更新。修改后的着色器代码如下,

#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;

out vec3 fColor;
void main(){
	gl_Position = vec4(aPosition + aOffset, 0.0, 1.0);
    fColor = aColor;
}

我们不再需要gl_InstanceID和offset了,而我们传入offset数据时和传入顶点和颜色的数据类似,如下

	……
	translations = new float[200];
    int index = 0;
    float offset = 0.1f;
	for (int y = -10; y < 10; y += 2) {
    
    
		for (int x = -10; x < 10; x += 2) {
    
    
			translations[index++] = (float) x / 10.0f + offset;
			translations[index++] = (float) y / 10.0f + offset;
    	}
	}
	……
	GLES20.glVertexAttribPointer(quadsRenderer.offsetHandle, 2, GLES20.GL_FLOAT, false, 2 * 4, OpenGLUtil.createFloatBuffer(translations));
	GLES30.glVertexAttribDivisor(quadsRenderer.offsetHandle, 1);
	GLES30.glDrawArraysInstanced(GLES20.GL_TRIANGLES, 0, 6, 100);

比较重要的是glVertexAttribDivisor( int index, int divisor )这个方法,它告诉了OpenGL该什么时候更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性,第二个参数是属性除数(Attribute Divisor)。默认情况下,属性除数是0,告诉OpenGL我们需要在顶点着色器的每次迭代时更新顶点属性。将它设置为1时,我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。而设置为2时,我们希望每2个实例更新一次属性,以此类推。我们将属性除数设置为1,是在告诉OpenGL,处于位置值2的顶点属性是一个实例化数组。
绘制后的效果和上面的效果相同。
实例化数组和gl_InstanceID 也可以结合使用,我们稍微修改下顶点着色器代码,根据gl_InstanceID 来缩小矩形,

	……
	void main(){
		float id = float(gl_InstanceID);
		vec2 pos = aPosition * (id / 100.0);
		gl_Position = vec4(pos + aOffset, 0.0, 1.0);
    	fColor = aColor;
	}

效果如下,
矩形变换

小行星带

宇宙中的某些星球会有星环或者行星带,它们都是由各种大小不一且位置不同的碎石组成的,在一定的范围内围绕着星球旋转,下面我们就实现这样一种效果。
关于行星和行星带的模型,可以在文章中的地址下载,分别是planet文件夹和rock文件夹,使用我们学习的模型加载进行加载。
首先我们不使用实例化来实现,着色器代码比较简单,输入顶点,纹理坐标和纹理即可。
顶点着色器

#version 300 es
layout (location = 0) in vec3 aPosition;
layout (location = 2) in vec2 aTexCoords;

out vec2 TexCoords;

uniform mat4 uMVPMatrix;

void main(){
    TexCoords = aTexCoords;
    gl_Position = uMVPMatrix * vec4(aPosition, 1.0f);
}

片段着色器

#version 300 es
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture_diffuse1;

void main(){
    FragColor = texture(texture_diffuse1, TexCoords);
}

然后需要计算小行星带上每个石块的位置,根据文档中的代码进行修改后如下

private float[][] createMatrices(int amount, float radius, float offset) {
    
    
        float[][] modelMatrices = new float[amount][16];
        Random random = new Random(System.nanoTime());

        for (int i = 0; i < amount; i++) {
    
    
            float[] modelMatrix = new float[16];
            Matrix.setIdentityM(modelMatrix, 0);
            // 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]
            float angle = (float) i / (float) amount * 360.0f;
            float displacement = (float) (random.nextInt((int) (2 * offset * 100))) / 100.0f - offset;
            float x = (float) Math.sin(Math.toRadians(angle)) * radius + displacement;
            displacement = (float) (random.nextInt((int) (2 * offset * 100))) / 100.0f - offset;
            float y = displacement * 0.4f;
            displacement = (float) (random.nextInt((int) (2 * offset * 100))) / 100.0f - offset;
            float z = (float) Math.cos(Math.toRadians(angle)) * radius + displacement;
            Matrix.translateM(modelMatrix, 0, x, y, z);
            // 2. 缩放:在 0.05 和 0.25f 之间缩放
            float scale = (float) (random.nextInt(20)) / 100.0f + 0.05f;
            Matrix.scaleM(modelMatrix, 0, scale, scale, scale);

            // 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
            float rotAngle = (float) random.nextInt(360);
            Matrix.rotateM(modelMatrix, 0, rotAngle, 0.4f, 0.6f, 0.8f);

            modelMatrices[i] = modelMatrix;
        }
        return modelMatrices;
    }

绘制的代码不再赘述,我们设置显示2000个,显示效果如下
普通方式
不使用实例化时,根据设备性能,绘制的速度不同,当逐渐增加显示数量时(例如10w),绘制时间很长,设备会非常卡。
下面我们使用实例化数组的方式来绘制,稍微修改一些参数(radius,offset)和观察点位置,设置数量为10w个。绘制小行星的代码无需变化,绘制行星带的顶点着色器需要修改,用一个4x4的矩阵aInstanceMatrix来接收变化矩阵数据,

#version 300 es
layout (location = 0) in vec3 aPosition;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 aInstanceMatrix;

out vec2 TexCoords;

uniform mat4 uVPMatrix;

void main(){
    TexCoords = aTexCoords;
    gl_Position = uVPMatrix * aInstanceMatrix * vec4(aPosition, 1.0f);
}

绘制行星带时传入变换数据,这里需要注意的是,由于mat4是一个4x4的矩阵,会生成四个句柄:3,4,5,6,只能四个一组进行传入,最后同样的要调用glVertexAttribDivisor,然后使用glDrawArraysInstanced绘制(因为我们的模型读取方法把索引变为了顶点,因此无法使用glDrawElementsInstanced),显示效果如下,

实例化数组实现
运行起来代码我们看到,10w个元素绘制的时间也不长,和不使用实例化增加到10w个元素的绘制时间对比一下可以看到差距很大。
可以尝试改变为一直绘制,可以看到行星带一直在围绕行星旋转。
当遇到这种场景时,可以尝试使用实例化来实现,例如雨水,草地,雪地等等。
本章源码地址

猜你喜欢

转载自blog.csdn.net/jklwan/article/details/104790722