OpenGL ES for Android(实例化)

作者: 不正经的创作者 | 来源:发表于2020-05-27 17:57 被阅读0次

简介

如果我们想要绘制许多相同的物体,只是他们的位置或大小不同,按照之前学习的知识,正常的做法是构建一个变化矩阵数组,通过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开发路上走的更好。

相关文章

网友评论

    本文标题:OpenGL ES for Android(实例化)

    本文链接:https://www.haomeiwen.com/subject/xscgahtx.html