基于OpenGL ES2.0 for Android。
一個(gè)程序需要有數(shù)據(jù)輸入,否則沒(méi)有實(shí)際意義,正如下面這段代碼。
void main() {
gl_Position = vec4(0);
}
盡管這個(gè)著色器是“格式良好的”,但是在實(shí)際應(yīng)用中,我們應(yīng)避免這種“寫(xiě)死在程序里”的數(shù)據(jù)。那么,如何往GLSL著色器中寫(xiě)入數(shù)據(jù)呢?
與C語(yǔ)言一樣,GLSL可以通過(guò)變量來(lái)接受應(yīng)用程序輸入的變量。但是GLSL中只有特定的修飾符修飾的變量才可以用于數(shù)據(jù)的輸入輸出。這些特定修飾符主要有attribute,varying,uniform。
首先是attribute修飾符。按照官方文檔介紹,attribute修飾的變量用于聲明基于每個(gè)頂點(diǎn)從OpenGL ES傳遞到頂點(diǎn)著色器的變量。這句話如何理解呢?我們知道,頂點(diǎn)著色器定義了圖形上頂點(diǎn)的位置,而各種圖形中除了“點(diǎn)”只有一個(gè)頂點(diǎn),其他圖形都具有一個(gè)以上的頂點(diǎn)。attribute修飾的變量就會(huì)根據(jù)頂點(diǎn)的數(shù)量依序設(shè)置和獲取頂點(diǎn)屬性數(shù)組的值。attribute所修飾的變量通常用來(lái)輸入頂點(diǎn)坐標(biāo)、法線等數(shù)據(jù)。
一、attribute的主要特性
- attribute修飾符只能用于頂點(diǎn)著色器中的變量,不能用于片元著色器中的變量。
//在fragment shader中使用attribute修飾符
attribute vec4 fColor;
void main() {
gl_FragColor = fColor;
}
attribute用于修飾片元著色器中的變量,編譯著色器源碼時(shí)會(huì)報(bào)錯(cuò)S0044: Attribute qualifier only allowed in vertex shaders。
- attribute只能修飾 float, vec2, vec3, vec4, mat2, mat3, 和 mat4等類(lèi)型。
attribute變量只能用于修飾以上幾種變量類(lèi)型,不能用于修飾其他變量。
- 就頂點(diǎn)著色器而言,attribute所修飾的變量是只讀的。只能通過(guò)OpenGL ES API來(lái)傳入和修改變量值。
//在vertex shader對(duì)attribute修飾符修飾的變量進(jìn)行賦值或修改
attribute vec4 vPosition;
void main() {
vPosition = vec4(1.0);
gl_Position = vPosition;
}
在著色器內(nèi)部對(duì)attribute修飾的變量賦值或修改,頂點(diǎn)著色器在編譯階段報(bào)錯(cuò): S0027: Cannot modify an input variable。
- attribute修飾的變量具有數(shù)量和內(nèi)存空間限制。
類(lèi)似于其他語(yǔ)言變量,attribute同樣有變量地址空間大小的限制。attribute變量通過(guò)一個(gè)很小的固定的存儲(chǔ)空間傳遞數(shù)據(jù),因此,GLSL規(guī)定了所有非矩陣類(lèi)型都有一個(gè)確定的不大于4個(gè)float大小(四個(gè)浮點(diǎn)數(shù)所需存儲(chǔ)空間)的存儲(chǔ)大小。矩陣類(lèi)型變量的存儲(chǔ)大小等于矩陣的行數(shù)乘以 4個(gè)float大小。
相應(yīng)地,由于總存儲(chǔ)空間大小固定且每個(gè)變量大小固定,GLSL限制了attribute所修飾的變量的最大計(jì)數(shù)(請(qǐng)注意,計(jì)數(shù)≠數(shù)量)。最大計(jì)數(shù)的值由提供的固定存儲(chǔ)空間的大小決定,但GLSL規(guī)定了此最大計(jì)數(shù)的最小值(GLSL規(guī)定此最大計(jì)數(shù)最小值為8)。這個(gè)最大計(jì)數(shù)是一個(gè)編譯時(shí)就確定的常量:gl_MaxVertexAttribs,我們可以通過(guò)OpenGL ES API獲取這個(gè)常量參數(shù)的值。
GLSL是如何計(jì)數(shù)地呢?我們可以簡(jiǎn)單地理解為,最大計(jì)數(shù)的值等于總存儲(chǔ)空間大小除以 4個(gè)float大小。所有非矩陣變量都計(jì)數(shù)1,即使這個(gè)變量小于4個(gè)float大小,矩陣類(lèi)型計(jì)數(shù)為矩陣的行數(shù)。如:一個(gè)float變量和一個(gè)vec4變量具有同樣的計(jì)數(shù)1,一個(gè)mat2變量計(jì)數(shù)為2,一個(gè)mat4變量計(jì)數(shù)為4。一個(gè)最大計(jì)數(shù)為8的OpenGL ES實(shí)現(xiàn)能夠分別存放8個(gè)float,四個(gè)mat2,兩個(gè)mat4。因此,將4個(gè)不相干的浮點(diǎn)數(shù)變量打包放到一個(gè)vec4變量中能夠最大限度利用硬件存儲(chǔ)空間。
另外,只聲明未使用的變量不計(jì)數(shù)。
我們可以簡(jiǎn)單的理解為,attribute變量存儲(chǔ)在一個(gè)由硬件廠商決定的固定大小的內(nèi)存緩沖區(qū)。它的最小計(jì)量單位為vec4。
通過(guò)OpenGL ES glGet* API可以獲取內(nèi)部常量參數(shù),下面的代碼獲取的是gl_MaxVertexAttribs(attribute修飾符修飾的變量的最大計(jì)數(shù))的值。
val parameters = IntArray(1)
GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, parameters, 0)
Log.e("parameters", parameters.contentToString())
打印結(jié)果parameters: [16]。表明此設(shè)備上的OpenGL ES內(nèi)置的最大attribute變量計(jì)數(shù)為16?,F(xiàn)在我們通過(guò)代碼測(cè)試超過(guò)這個(gè)計(jì)數(shù)會(huì)發(fā)生什么。
val vertexCode =
"attribute mat4 a;\n" +
"attribute mat4 b;\n" +
"attribute mat4 c;\n" +
"attribute mat4 d;\n" +
"attribute vec4 e;\n" +
"void main() {\n" +
" gl_Position = a*b*c*d*e;\n" +
"}"
val fragmentCode =
"void main() {\n" +
" gl_FragColor = vec4(0.5);\n" +
"}"
linkProgram(vertexCode, fragmentCode)
執(zhí)行報(bào)錯(cuò),L0004 Not able to fit attributes。錯(cuò)誤代碼L0004,表示Linker(鏈接器)第四號(hào)錯(cuò)誤。我們可以去官方文檔查找一下它的詳細(xì)介紹。
L0004: Too many attribute values.正是attribute計(jì)數(shù)超過(guò)限制。
現(xiàn)在我們簡(jiǎn)單地修改一下void main()函數(shù),將" gl_Position = a*b*c*d*e;\n"修改為" gl_Position = a*b*c*e;\n",去掉了參數(shù)d的使用,將使用參數(shù)計(jì)數(shù)減少到13。執(zhí)行結(jié)果表明一切正常。
- attribute變量不能以“gl_”為前綴。
val vertexCode = """
attribute vec4 gl_a;
void main() {
gl_Position = gl_a;
}
"""
val fragmentCode = """
void main() {
gl_FragColor = vec4(0.5);
}
"""
linkProgram(vertexCode, fragmentCode)
鏈接時(shí)報(bào)錯(cuò):L0006: Illegal identifier name 'gl_a'
- attribute變量作用域必須是全局的。
attribute的作用域必須是全局的(即必須在方法外聲明),否則編譯時(shí)會(huì)報(bào)錯(cuò):S0045: Attribute declared inside a function
二、attribute變量的應(yīng)用
了解了attribute修飾符的特性后,我們來(lái)學(xué)習(xí)如何使用它。
1. 獲取attribute變量的地址索引
我們可以通過(guò)glGetAttribLocation(int program, String name)獲取一個(gè)attribute變量的地址索引。
int glGetAttribLocation(int program, String name)
program:已鏈接的SL程序?qū)ο蟆?br>name:attribute變量名。
return:返回attribute變量表中的位置。請(qǐng)注意,這個(gè)方法必須在
glLinkProgram(int program)之后調(diào)用才有效,否則返回索引值為-1。另外,當(dāng)attribute變量未處于active狀態(tài)(只聲明未使用或使用無(wú)意義,這種情況的attribute變量在編譯時(shí)會(huì)被優(yōu)化忽略),glGetAttribLocation(int program, String name)返回值也是-1。
val vertexCode = """
attribute mat4 a;
attribute mat4 b;
attribute float c;
attribute mat4 d;
attribute vec4 e;
attribute vec4 f;
void main() {
vec4 g = f;
gl_Position = a*c*d*e;
}
"""
val fragmentCode = """
void main() {
gl_FragColor = vec4(0.5);
}
"""
val program = linkProgram(vertexCode, fragmentCode)
val locations = ('a'..'f').joinToString {
"location${it.toUpperCase()}: ${GLES20.glGetAttribLocation(program, it.toString())}"
}
Log.e("locations", locations)
打印結(jié)果locations: locationA: 0, locationB: -1, locationC: 8, locationD: 4, locationE: 9, locationF: -1。
從這個(gè)結(jié)果我們可以看到,只聲明未使用的變量b和使用無(wú)意義的變量f返回的索引均為-1。
另外,仔細(xì)觀察其他幾個(gè)變量的索引我們會(huì)發(fā)現(xiàn)它符合上面講到的第五條特性。并且,如果地址索引在一定程度上反應(yīng)了變量在物理空間的相對(duì)位置,那么我們可以大膽猜測(cè),OpenGL ES/GLSL為了更好的利用硬件存儲(chǔ)空間,并沒(méi)有按照我們聲明的順序存儲(chǔ)變量,而是按地址空間大小從大到小存儲(chǔ)。此處,我們暫且不去討論OpenGL ES或者GLSL規(guī)定的存儲(chǔ)方式,留待以后再討論。
2. 手動(dòng)設(shè)置attribute變量的地址索引
除了在鏈接時(shí)自動(dòng)為attribute變量分配地址索引外,GLSL還允許我們?cè)阪溄映绦蚯巴ㄟ^(guò)glBindAttribLocation(int, int, String)手動(dòng)為attribute變量設(shè)置地址索引。
void glBindAttribLocation(int program, int index, String name)
program:未鏈接的SL程序?qū)ο蟆?br>index:想要的設(shè)置的attribute變量在attribute變量表中的位置,即glGetAttribLocation(int program, String name)的返回結(jié)果。它必須是一個(gè)0到gl_MaxVertexAttribs-1的值,否則此次方法調(diào)用無(wú)效。
name:attribute變量名。這個(gè)方法必須在
glLinkProgram(int program)調(diào)用前使用,否則設(shè)置不會(huì)生效。
val vertexCode = """
attribute mat4 a;
attribute mat4 b;
attribute mat4 c;
void main() {
gl_Position = a[0]+b[0]+c[0];
}
"""
val fragmentCode = """
void main() {
gl_FragColor = vec4(0.5);
}
"""
val program = attatchShaders(vertexCode, fragmentCode)
//valid
GLES20.glBindAttribLocation(program, 2, "a")
//exceed,gl_MaxVertexAttribs = 16
GLES20.glBindAttribLocation(program, 18, "b")
//linking
GLES20.glLinkProgram(program)
//valid, but after link
GLES20.glBindAttribLocation(program, 1, "c")
val locations = ('a'..'c').joinToString {
"location${it.toUpperCase()}: ${GLES20.glGetAttribLocation(program, it.toString())}"
}
Log.e("locations", locations)
打印結(jié)果locations: locationA: 2, locationB: 6, locationC: 10。
可以看到,b雖然在程序鏈接前綁定,但index越界。c在程序鏈接后綁定,此時(shí)attribute變量表已經(jīng)生成,改變無(wú)效。只有對(duì)變量a的設(shè)置是有效的,b、c的地址索引依然是鏈接器分配的索引。
3. 修改attribute變量的值
OpenGL ES提供了兩個(gè)API為attribute變量設(shè)置變量值,它們分別是glVertexAttribPointer( int indx, int size, int type, boolean normalized, int stride, int offset )和glVertexAttribPointer( int indx, int size, int type, boolean normalized, int stride, java.nio.Buffer ptr ),其中前一個(gè)方法是從VertexBufferObject為attribute變量賦值,后一個(gè)方法是從主內(nèi)存為attribute變量賦值,前一個(gè)方法留到后面講,此處我們主要了解后一個(gè)方法。
void glVertexAttribPointer(int indx, int size, int type, boolean normalized, int stride, java.nio.Buffer ptr)
index:所要設(shè)置的變量的地址位置。由glGetAttribLocation(int program, String name)得到。
size:每個(gè)頂點(diǎn)所需要的元素個(gè)數(shù)。必須是1,2,3,4中的一個(gè)值,默認(rèn)為4。
type:表示數(shù)組中每個(gè)元素的數(shù)據(jù)類(lèi)型。必須是GL_BYTE,GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT,GL_FIXED或GL_FLOAT中的一個(gè),默認(rèn)為GL_FLOAT。
normalized:表示數(shù)據(jù)是否需要在傳入到頂點(diǎn)數(shù)組前進(jìn)行歸一化處理。
stride:表示ptr中兩個(gè)連續(xù)元素之間的偏移字節(jié)數(shù)。如果stride為0,那么則表示ptr中個(gè)元素是緊密貼合在一起的。
ptr:需要設(shè)置的數(shù)據(jù)的內(nèi)存緩存。為了最大限度提高效率,建議使用ByteBuffer裝載數(shù)據(jù)。需要注意的是,不同存儲(chǔ)設(shè)備存取字節(jié)順序有兩種不同方式,分別是Big-Endian(高位優(yōu)先)或Little-Endian(低位優(yōu)先),Android中是Little-Endian。因此,我們需要使用當(dāng)前運(yùn)行設(shè)備上的存取字節(jié)順序,這可以通過(guò)ByteOrder.nativeOrder()方法獲得。另外,在OpenGL訪問(wèn)前還需要調(diào)用
glEnableVertexAttribArray(int)方法啟用該通用頂點(diǎn)數(shù)組。否則glVertexAttribPointer()調(diào)用無(wú)效,OpenGL會(huì)使用靜態(tài)頂點(diǎn)屬性值。
fun vertexAttribPointer(program: Int, name: String, size: Int, pType: Int,
normalized: Boolean, stride: Int, ptr: FloatArray): Int {
return glGetAttribLocation(program, name).also {
//為頂點(diǎn)屬性傳入數(shù)據(jù)
glVertexAttribPointer(it, size, pType, normalized, stride, ptr.toBuffer())
glEnableVertexAttribArray(it)
}
}
fun FloatArray.toBuffer(): FloatBuffer {
return ByteBuffer.allocateDirect(size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
.put(this).position(0) as FloatBuffer
}
三、本文使用的工具方法
fun loadShader(type: Int, code: String): Int {
val shader = GLES20.glCreateShader(type)
GLES20.glShaderSource(shader, code)
GLES20.glCompileShader(shader)
Log.e("loadShader", "type: $type, shader: $shader, error: ${GLES20.glGetShaderInfoLog(shader)}")
Log.e("loadShader", "shaderSource: ${GLES20.glGetShaderSource(shader)}")
return shader
}
fun attatchShaders(vertexShader: Int, fragmentShader: Int): Int {
val program = GLES20.glCreateProgram()
GLES20.glAttachShader(program, vertexShader)
Log.e("attatchShaders", "shader: $vertexShader, error: ${GLES20.glGetProgramInfoLog(program)}")
GLES20.glAttachShader(program, fragmentShader)
Log.e("attatchShaders", "shader: $fragmentShader, error: ${GLES20.glGetProgramInfoLog(program)}")
return program
}
fun attatchShaders(vertexCode: String, fragmentCode: String): Int {
val vertexHandle = loadShader(GLES20.GL_VERTEX_SHADER, vertexCode)
val fragmentHandle =loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode)
return attatchShaders(vertexHandle, fragmentHandle)
}
fun linkProgram(vertexShader: Int, fragmentShader: Int): Int {
val program = attatchShaders(vertexShader, fragmentShader)
GLES20.glLinkProgram(program)
Log.e("linkProgram", "program: $program, error: ${GLES20.glGetProgramInfoLog(program)}")
return program
}
fun linkProgram(vertexCode: String, fragmentCode: String): Int {
val program = attatchShaders(vertexCode, fragmentCode)
GLES20.glLinkProgram(program)
Log.e("linkProgram", "program: $program, error: ${GLES20.glGetProgramInfoLog(program)}")
return program
}