简介
如果我们想要绘制许多相同的物体,只是他们的位置或大小不同,按照之前学习的知识,正常的做法是构建一个变化矩阵数组,通过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个元素的绘制时间对比一下可以看到差距很大。
可以尝试改变为一直绘制,可以看到行星带一直在围绕行星旋转。
当遇到这种场景时,可以尝试使用实例化来实现,例如雨水,草地,雪地等等。
本章源码地址。
辛苦路过的攻城师们点个赞,相互交流学习下。希望各位在未来android开发路上走的更好。
网友评论