學習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個元素的繪製時間對比一下可以看到差距很大。
可以嘗試改變爲一直繪製,可以看到行星帶一直在圍繞行星旋轉。
當遇到這種場景時,可以嘗試使用實例化來實現,例如雨水,草地,雪地等等。
本章源碼地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章