android平臺下OpenGL ES 3.0著色語言基礎知識(下)
OpenGL ES 3.0學習實踐
- android平臺下OpenGL ES 3.0從零開始
- android平臺下OpenGL ES 3.0繪製純色背景
- android平臺下OpenGL ES 3.0繪製圓點、直線和三角形
- android平臺下OpenGL ES 3.0繪製彩色三角形
- android平臺下OpenGL ES 3.0從矩形中看矩陣和座標系
- android平臺下OpenGL ES 3.0著色語言基礎知識(上)
本篇整理自《OpenGL ES 3.0 程式設計指南第2版》
目錄
統一變數和屬性
統一變數(uniform
)是儲存應用程式通過OpenGL ES 3.0 API
傳遞給著色器的只讀常數值的變數。
統一變數
被組合成兩類統一變數塊。
- 第一類是命名統一變數塊,統一變數的值由所謂的統一變數緩衝區物件支援,命名統一變數塊被分配一個
統一變數塊索引
。
uniform TransformBlock {
mat4 matViewProj;
mat3 matNormal;
mat3 matTexGen;
};
- 第二類是預設的統一變數塊,用於在命名統一變數塊之外宣告的統一變數。和
命名統一變數塊
uniform mat4 matViewProj;
uniform mat3 matNormal;
uniform mat3 matTexGen;
獲取和設定統一變數
要査詢程式中活動統一變數
的列表,首先要用GL_ACTIVE_UNIFORMS
引數,呼叫glGetProgramiv
。這樣可以獲得程式中活動統一變數的數量。這個列表包含命名統一變數塊中的統一變數
、著色器程式碼中宣告的預設統一變數塊中的統一變數以及著色器程式碼中使用的內建統一變數
。如果統一變數被程式使用,就認為它是"活動"
的。換言之,如果你在一個著色器中聲明瞭一個統一變數但是從未使用,連結程式可能會在優化時將其去掉,不在活動統一變數列表中返回
glGetActiveUniform
和glGetActiveUniformsiv
找出每個統一變數的細節。
使用glGetActiveUniform
,可以確定幾乎所有統一變數的屬性。你可以確定統一變數的名稱和型別
。此外,可以發現變數是不是陣列以及陣列中使用的最大元素。統一變數的名稱 對於找到統一變數的位置是必要的
,要知道如何載入統一變數的資料,需要統一變數的型別和大小。一旦有了統一變數的名稱,就可以用glGetUniformLocation
找到它的位置。統一變數的位置是一個整數值,用於標識統一變數在程式中的位置(注意: 命名統一變數塊中的統一變數沒有指定位置
)。這個位置值用於載入統一變數值的後續呼叫(例如: glUniformlf
)。
public static native int glGetUniformLocation(
int program,
String name
);
這個函式將返回由name
指定的統一變數的位置。如果這個統一變數不是程式中的活動統一變數,返回值將為-1。有了統一變數的位置及其型別和陣列大小,我們就可以載入統一變數的值。載入統一變數值有許多不同的函式,每種統一變數型別都對應不同的函式。
public static native void glUniform1f(int location,float x);
public static native void glUniform1fv(int location,int count,float[] v,int offset);
......
載入統一變數所需的函式根據glGetActiveUniform
函式返回的type
確定。例如,如果返回的型別是GL_FLOAT_VEC4
, 那麼可以使用glUniform4f
或glUnifomi4fv
。如果gIGetActiveUniform
返回的size
大於 1, 則使用glUnifrom4fv
在一次呼叫中載入整個陣列。如果統一變數不是陣列,則可以使用glUniform4f
或glUniform4fv
注意: glUniform*
呼叫不以程式物件控制代碼作為引數。原因是: glUniform*
總是在與glUseProgram
繫結的當前程式上操作。統一變數值本身儲存在程式物件中。也就是說,一旦在程式物件中設定一個統一變數的值,即使你讓另一個程式處於活動狀態,該值仍然保留在原來的程式物件中。從這個意義上,我們可以說統一變數值是程式物件區域性所有的。
下面來實踐一下査詢程式物件中的統一變數資訊的方法。
新建一個UniformRenderer.java
檔案:
/**
* @anchor: andy
* @date: 2018-11-02
* @description:
*/
public class UniformRenderer implements GLSurfaceView.Renderer {
private static final String TAG = "UniformRenderer";
private int mProgram;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//設定背景顏色
GLES30.glClearColor(0.5f, 0.5f, 0.5f, 0.5f);
//編譯
final int vertexShaderId = ShaderUtils.compileVertexShader(ResReadUtils.readResource(R.raw.vertex_uniform_shader));
final int fragmentShaderId = ShaderUtils.compileFragmentShader(ResReadUtils.readResource(R.raw.fragment_uniform_shader));
//連結程式片段
mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId);
//在OpenGLES環境中使用程式片段
GLES30.glUseProgram(mProgram);
final int[] maxUniforms = new int[1];
GLES30.glGetProgramiv(mProgram, GLES30.GL_ACTIVE_UNIFORM_MAX_LENGTH, maxUniforms, 0);
final int[] numUniforms = new int[1];
GLES30.glGetProgramiv(mProgram, GLES30.GL_ACTIVE_UNIFORMS, numUniforms, 0);
Log.d(TAG, "maxUniforms=" + maxUniforms[0] + " numUniforms=" + numUniforms[0]);
int[] length = new int[1];
int[] size = new int[1];
int[] type = new int[1];
byte[] nameBuffer = new byte[maxUniforms[0] - 1];
for (int index = 0; index < numUniforms[0]; index++) {
GLES30.glGetActiveUniform(mProgram, index, maxUniforms[0], length, 0, size, 0, type, 0, nameBuffer, 0);
String uniformName = new String(nameBuffer);
int location = GLES30.glGetUniformLocation(mProgram, uniformName);
Log.d(TAG, "uniformName=" + uniformName + " location=" + location + " type=" + type[0] + " size=" + size[0]);
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES30.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
}
}
頂點著色器
#version 300 es
uniform mat4 mMatrix4;
uniform mat3 mMatrix3;
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec4 aColor;
out vec4 vColor;
void main() {
gl_Position = mMatrix4 * vPosition;
gl_PointSize = 10.0;
vColor = aColor;
}
片段著色器
#version 300 es
precision mediump float;
in vec4 vColor;
out vec4 fragColor;
void main() {
fragColor = vColor;
}
輸出如下日誌:
11-07 11:46:22.099 28987-29005/? D/UniformRenderer: maxUniforms=9 numUniforms=1
11-07 11:46:22.099 28987-29005/? D/UniformRenderer: uniformName=mMatrix4 location=0 type=35676 size=1
為什麼只輸出了
mMatrix4
,那mMatrix3
呢? 剛剛也說到了,雖然我們在頂點著色器中聲明瞭mMatrix3
,但是我們並沒有使用它,導致它被連結程式優化掉了
統一變數塊
統一變數緩衝區物件
可以通過一個緩衝區物件
支援統一變數資料的儲存。統一變數緩衝區物件
在某些條件下比單獨的統一變數有更多優勢。利用統一變數緩衝區物件
,統一變數緩衝區資料
可以在多個程式中共享,但只需要設定一次。而且統一變數緩衝區物件
一般可以儲存更大量的統一變數資料
。最後,在統一緩衝區物件
之間切換比一次單獨載入一個統一變最更高效。
統一緩衝區物件
可以在OpenGL ES
著色語言中通過應用統一變數塊使用。
uniform TransformBlock {
mat4 matViewProj;
mat3 matNormal;
mat3 matTexGen;
};
上述聲明瞭一個名為TransformBlock
且包含3個矩陣的統一變數塊。名稱TransformBlock
將供應用程式使用,統一緩衝區物件函式glGetUniformBlocklndex
中的blockName
引數。統一變數塊宣告中的變數在著色器中都可以訪問,就像常規形式宣告的變數一樣。
#version 300 es
uniform TransformBlock {
mat4 matViewProj;
mat3 matNormal;
mat3 matTexGen;
};
layout(location = 0) in vec4 a_position;
void main{
gl_Position = matViewProj * a_position;
}
佈局限定符
可用於指定支援統一變數塊的統一緩衝區物件在記憶體中的佈局方式。佈局限定符
可以提供給單獨的統一變數塊
,或者用於所有統一變數塊。在全域性作用域內,為所有統一變數塊設定預設佈局的方法如下:
layout(shared, column_major) uniform; // 如果未指定,則為預設
layout(packed, row_major) uniform;
單獨的統一變數塊也可以通過覆蓋全域性作用域上的預設設定來設定佈局。此外統一變數塊中的單獨統一變數也可以指定佈局限定符
layout(stdl40) uniform TransformBlock
{
mat4 matViewProj;
layout(row_major) mat3 matNormal;
mat3 matTexGen;
};
可以用於統一變數塊的所有佈局限定符:
限定符 | 描述 |
---|---|
shared |
shared 限定符指定多個著色器或者多個程式中統一變數塊的記憶體佈局相同。要使用這個限定符,不同定義中的row_major/column_major 值必須相等。覆蓋stdl40 和packed (預設) |
packed |
packed 佈局限定符指定編譯器可以優化統一變數塊的記憶體佈局。使用這個限定符時必須查詢偏移位置,而且統一變數塊無法在頂點/片段著色器或者程式間共享。覆蓋stdl40 和shared |
stdl40 |
sldl40 佈局限定符指定統一變童塊的佈局基於OpenGL ES 3.0 規範中定義的一組標準規則。覆蓋shared 和packed |
row_major |
矩陣在記憶體中以行優先順序佈局 |
column_major |
矩陣在記憶體中以列優先順序佈局(預設) |
頂點和片段著色器輸入/輸出
OpenGL ES
著色器語言的另一個特殊變數型別是頂點輸入(或者屬性)
變數。頂點輸入變數用於指定頂點著色器中每個頂點的輸入,用in關鍵字
指定。它們通常儲存位置、法線、 紋理座標和顏色
這樣的資料。這裡的關鍵是理解頂點輸入是為繪製的每個頂點指定的資料。
頂點著色器
#version 300 es
uniform mat4 u_matViewProjection;
//輸入
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec3 a_color;
out vec3 v_color;
void main{)
{
gl_Position = u_matViewProjection * a_position;
v_color = a_color;
}
片段著色器
#version 300 es
precision mediump float;
in vec4 vColor;
//輸出片段著色器
layout (location = 0) out vec4 fragColor;
void main() {
fragColor = vColor;
}
這個著色器的兩個頂點輸入變數a_position和a_color
的資料由應用程式載入。本質上,應用程式將為每個頂點建立一個頂點陣列,該陣列包含位置和顏色
。注意上面的例子中頂點輸入變數之前使用了layout限定符
。這種情況下的佈局限定符用於指定頂點屬性的索引。佈局限定符是可選的,如果沒有指定,連結程式將自動為頂點輸入變數分配位置
。
和統一變數
—樣,底層硬體通常在可輸入頂點著色器的屬性變數數目上有限制
。OpenGL ES實現支援的最大屬性數量由內建變數gl_MaxVertexAttribs
給出(也可以使用glGetlntegerv
査詢GL_MAX_VERTEX_ATTRIBS
得到)。OpenGL ES 3.0實現可支援的最小屬性為16個
。 不同的實現可以支援更多變數,但是如果想要編寫保證能在任何OpenGL ES 3.0實現上執行的著色器,則應該將屬性限制為不多於16個。
來自頂點著色器的輸出變數
由out關鍵字
指定。上面的示例程式碼中,v_color
變數被宣告為輸出變數,其內容從a_color輸入變數
中複製而來。每個頂點著色器將在一個或者多個輸出變數中輸出需要傳遞給片段著色器的資料。然後,這些變數也會在片段著色器中宣告為in變數
(相符型別),在光柵化階段中對圖元進行線性插值
。
片段著色器
中與頂點著色器
的頂點輸出v_Color
相匹配的輸入宣告如下:
in vec3 v_color;
注意:
與頂點著色器輸入不同,頂點著色器輸出/片段著色器輸入變數不能有佈局限定符
。OpenGL ES
實現自動選擇位置,與統一變數和頂點輸入屬性相同,底層硬體通常限制頂點著色器輸出/片段著色器輸入
(在硬體上,這些變數通常被稱作插值器)的數量。 OpenGL ES實現支援的頂點著色器輸出的數量由內建變數gl_MaxVertexOutputVectors
給出(用glGetlntegerv
査詢GL_MAX_VERTEX_OUTPUT_COMPONENTS
將提供總分量值數量,而非向量數量)。OpenGLES 3.0實現可以支援的最小頂點輸出向量數為16
。與此類似,OpenGL ES 3.0實現支援的片段著色器輸入的數量由gl_MaxFragmentInputVectors
給出(用glGetlntegerv
查詢GL_MAX_FRAGMENT_INPUT_COMPONENTS
將提供總分量值數量,而非向量數量)。OpenGL ES 3.0實現可以支援的最小片段輸入向量數為15
。
上述的片段著色器將輸出一個或者多個顏色。在正常情況下,我們只渲染到一個顏色緩衝區,在這種時候,佈局限定符是可選的(假定輸出變數進入位置0)
。但是,當渲染到多個渲染目標(MRT)
時,我們可以使用佈局限定符指定每個輸出前往的渲染目標。對於這種情況,在片段著色器中會有個輸出變數,該值將是傳遞給管線逐片段操作部分的輸出顏色。
插值限定符
上面的示例中,我們聲明瞭自己的頂點著色器輸出和片段著色器輸入,沒有使用任何限定符。在沒有限定符時,預設的插值行為是執行平滑著色。也就是說,來自頂點著色器的輸出變數在圖元中線性插值,片段著色器接收線性插值之後的數值作為輸入
。我們可以明確地請求平滑著色,在這種情況下,輸出/輸入如下:
// 頂點著色器輸出
smooth out vec3 v_color;
// 片段著色器輸入
smooth in vec3 v_color;
OpenGL ES 3.0
還引入了另一種插值——平面著色
。在平面著色中,圖元中的值沒有進行插值,而是將其中一個頂點視為驅動頂點(Provoking Vertex,取決於圖元型別),該頂點的值被用於圖元中的所有片段。我們可以宣告如下的平面著色輸出/ 輸入:
// 頂點著色器輸出
flat out vec3 v_color;
// 片段著色器輸入
flat in vec3 v_color;
最後,可以用centroid
關鍵字在插值器中新增另一個限定符。使用多重取樣渲染時,centroid關鍵字
可用於強制插值發生在被渲染圖元內部(否則,在圖元的邊緣可能出現偽像)。
質心取樣
的輸出/輸入變數的方法。
// 頂點著色器輸出
smooth centroid out vec3 v_color
// 頂點著色器輸出
smooth centroid in vec3 v_color
統一變數和插值器打包
底層硬體中可用於每個變數儲存的資源是固定的。統一變數通常儲存在所謂的"常量儲存"
中,這可以看作向量的物理陣列。頂點著色器輸出/片段著色器輸入一般儲存在插值器中
,這通 常也儲存為一個向量陣列。著色器可能宣告各種型別的統一變數和著色器輸入/輸出,包括標量、各種向量分量和矩陣。但是,這些變數宣告如何對映到硬體上的可用物理空間呢?換言之,如果一個OpenGL ES 3.0
實現支援16個頂點著色器輸出向量,那 麼物理儲存實際上是如何使用的呢?
在OpenGL ES 3.0中,這個問題通過打包規則處理,該規則定義插值器和統一變數對映到物理儲存空間的方式
。打包規則基於物理儲存空間被組織為一個每個儲存位置4列(每個向量分量一列)和1行的網格的概念。打包規則尋求打包變數,使生成程式碼的複雜度保持不變。換言之,打包規則不進行重排序操作,而是試圖在不對執行時效能產生負面影響的情況下,優化實體地址空間的使用。
uniform mat3 m;
uniform float f[6];
uniform vec3 v;
如果完全不進行打包,許多常量儲存空間將被浪費。矩陣m
將佔據3行,陣列f
佔據6行,向量v
佔據1行,共需要10行才能儲存這些變數。
未打包的統一變數儲存
位置 | X | Y | Z | W |
---|---|---|---|---|
0 | m[0].x | m[0].y | m[0].z | m[0].w |
1 | m[1].x | m[1].y | m[1].z | m[1].w |
2 | m[2].x | m[2].y | m[2].z | m[2].w |
3 | f[0] | - | - | - |
4 | f[1] | - | - | - |
5 | f[2] | - | - | - |
6 | f[3] | - | - | - |
7 | f[4] | - | - | - |
8 | f[5] | - | - | - |
9 | v.x | v.y | v.z | -6 |
打包的統一變數儲存
位置 | X | Y | Z | W |
---|---|---|---|---|
0 | m[0].x | m[0].y | m[0].z | f[0] |
1 | m[1].x | m[1].y | m[1].z | f[1] |
2 | m[2].x | m[2].y | m[2].z | f[2] |
3 | v.x | v.y | v.z | f[3] |
4 | - | - | - | f[4] |
5 | - | - | - | f[5] |
在使用打包規則時,只需使用6個物理常量位置。陣列f
的元素會跨越行的邊界,原因是GPU通常會按照向量位置索引對常量儲存進行索引。打包必須使陣列跨越行邊界,這樣索引才能夠起作用。
所有打包對OpenGL ES著色語言的使用者都是完全透明的,除了一個細節:打包影響統一變數和頂點著色器輸出/片段著色器輸入的計數方式
。如果想要編寫保證能夠在所有OpenGL ES 3.0實現上執行的著色器,就不應該使用打包之後超過最小執行儲存大小的統一變數或者插值器。
精度限定符
梢度限定符使著色器創作者可以指定著色器變數的計算精度。變數可以宣告為低、中或者高精度。這些限定符用於提示編譯器允許在較低的範圍和精度上執行變數計算。在較低的精度上,有些OpenGLES實現在執行著色器時可能更快,或者電源效率更高。
當然,這種效率提升是以精度為代價的,在沒有正確使用精度限定符時可能造成偽像。
精度限定符可以用於指定任何基於浮點數或者整數的變數的精度
。指定精度的關鍵字是lowp、mediump和highp
。
highp vec4 position;
varying lowp vec4 color;
mediump float specularExp;
如果變數宣告時沒有使用精度限定符,它將擁有該型別的預設精度
。預設精度限定符在頂點或者片段著色器的開頭指定:
precision highp float;
precision mediump int;
為float型別
指定的精度將用作所有基於浮點值的變數的預設精度。同樣,為int型別
指定的精度將用作所有基於整數的變數的預設精度。
在頂點著色器中,如果沒有指定預設精度,則int和float的預設精度都為highp。也就是說,頂點著色器中所有沒用精度限定符宣告的變數都使用最高的精度
。片段著色器的規則與此不同。在片段著色器中,浮點值沒有預設的精度值,每個著色器必須宣告一個預設的float精度,或者為每個float變數指定精度。
不變性
OpenGL ES著色語言中引入的invariant關鍵字
可以用於任何可變的頂點著色器輸出。
由於著色器需要編譯,而編譯器可能進行導致指令重新排序的優化。這種指令重排意味著兩個著色器之間的等價計算不能保證產生完全相同的結果
。這種不一致性在多遍著色器特效時尤其可能成為問題,在這種情況下,相同的物件用Alpha混合繪製在自身上方。如果用於計算輸出位置的數值的精度不完全一樣,精度差異就會導致偽像。
因為編譯器需要保證不變性,所以可能限制它所做的優化。因此,invariant限定符
應該只在必要時使用,否則可能導致效能下降。