基于OpenGL ES2.0 for Android。
一個程序需要有數(shù)據(jù)輸入茄厘,否則沒有實際意義矮冬,正如下面這段代碼。
void main() {
gl_Position = vec4(0);
}
盡管這個著色器是“格式良好的”次哈,但是在實際應(yīng)用中胎署,我們應(yīng)避免這種“寫死在程序里”的數(shù)據(jù)。那么窑滞,如何往GLSL著色器中寫入數(shù)據(jù)呢琼牧?
與C語言一樣恢筝,GLSL可以通過變量來接受應(yīng)用程序輸入的變量。但是GLSL中只有特定的修飾符修飾的變量才可以用于數(shù)據(jù)的輸入輸出巨坊。這些特定修飾符主要有attribute
撬槽,varying
,uniform
趾撵。
首先是attribute修飾符侄柔。按照官方文檔介紹,attribute修飾的變量用于聲明基于每個頂點從OpenGL ES傳遞到頂點著色器的變量占调。這句話如何理解呢暂题?我們知道,頂點著色器定義了圖形上頂點的位置究珊,而各種圖形中除了“點”只有一個頂點薪者,其他圖形都具有一個以上的頂點。attribute修飾的變量就會根據(jù)頂點的數(shù)量依序設(shè)置和獲取頂點屬性數(shù)組的值剿涮。attribute所修飾的變量通常用來輸入頂點坐標(biāo)言津、法線等數(shù)據(jù)。
一取试、attribute的主要特性
- attribute修飾符只能用于頂點著色器中的變量悬槽,不能用于片元著色器中的變量。
//在fragment shader中使用attribute修飾符
attribute vec4 fColor;
void main() {
gl_FragColor = fColor;
}
attribute用于修飾片元著色器中的變量想括,編譯著色器源碼時會報錯S0044: Attribute qualifier only allowed in vertex shaders
陷谱。
- attribute只能修飾 float, vec2, vec3, vec4, mat2, mat3, 和 mat4等類型。
attribute變量只能用于修飾以上幾種變量類型瑟蜈,不能用于修飾其他變量烟逊。
- 就頂點著色器而言,attribute所修飾的變量是只讀的铺根。只能通過OpenGL ES API來傳入和修改變量值宪躯。
//在vertex shader對attribute修飾符修飾的變量進行賦值或修改
attribute vec4 vPosition;
void main() {
vPosition = vec4(1.0);
gl_Position = vPosition;
}
在著色器內(nèi)部對attribute修飾的變量賦值或修改,頂點著色器在編譯階段報錯: S0027: Cannot modify an input variable
位迂。
- attribute修飾的變量具有數(shù)量和內(nèi)存空間限制访雪。
類似于其他語言變量,attribute同樣有變量地址空間大小的限制掂林。attribute變量通過一個很小的固定的存儲空間傳遞數(shù)據(jù)臣缀,因此,GLSL規(guī)定了所有非矩陣類型都有一個確定的不大于4個float大小
(四個浮點數(shù)所需存儲空間)的存儲大小泻帮。矩陣類型變量的存儲大小等于矩陣的行數(shù)乘以 4個float大小
精置。
相應(yīng)地,由于總存儲空間大小固定且每個變量大小固定锣杂,GLSL限制了attribute所修飾的變量的最大計數(shù)(請注意脂倦,計數(shù)≠數(shù)量)番宁。最大計數(shù)的值由提供的固定存儲空間的大小決定,但GLSL規(guī)定了此最大計數(shù)的最小值(GLSL規(guī)定此最大計數(shù)最小值為8)赖阻。這個最大計數(shù)是一個編譯時就確定的常量:gl_MaxVertexAttribs
蝶押,我們可以通過OpenGL ES API獲取這個常量參數(shù)的值。
GLSL是如何計數(shù)地呢火欧?我們可以簡單地理解為棋电,最大計數(shù)的值等于總存儲空間大小除以 4個float大小
。所有非矩陣變量都計數(shù)1苇侵,即使這個變量小于4個float大小
离陶,矩陣類型計數(shù)為矩陣的行數(shù)。如:一個float
變量和一個vec4
變量具有同樣的計數(shù)1衅檀,一個mat2
變量計數(shù)為2,一個mat4
變量計數(shù)為4霎俩。一個最大計數(shù)為8的OpenGL ES實現(xiàn)能夠分別存放8個float
哀军,四個mat2
,兩個mat4
打却。因此杉适,將4個不相干的浮點數(shù)變量打包放到一個vec4
變量中能夠最大限度利用硬件存儲空間。
另外柳击,只聲明未使用的變量不計數(shù)猿推。
我們可以簡單的理解為,attribute變量存儲在一個由硬件廠商決定的固定大小的內(nèi)存緩沖區(qū)捌肴。它的最小計量單位為vec4
蹬叭。
通過OpenGL ES glGet* API可以獲取內(nèi)部常量參數(shù),下面的代碼獲取的是gl_MaxVertexAttribs
(attribute修飾符修飾的變量的最大計數(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變量計數(shù)為16。現(xiàn)在我們通過代碼測試超過這個計數(shù)會發(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í)行報錯坦喘,L0004 Not able to fit attributes
。錯誤代碼L0004西设,表示Linker(鏈接器)第四號錯誤瓣铣。我們可以去官方文檔查找一下它的詳細(xì)介紹。
L0004: Too many attribute values.
正是attribute計數(shù)超過限制贷揽。
現(xiàn)在我們簡單地修改一下void main()
函數(shù)棠笑,將" gl_Position = a*b*c*d*e;\n"
修改為" gl_Position = a*b*c*e;\n"
,去掉了參數(shù)d
的使用擒滑,將使用參數(shù)計數(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)
鏈接時報錯:L0006: Illegal identifier name 'gl_a'
- attribute變量作用域必須是全局的藻糖。
attribute的作用域必須是全局的(即必須在方法外聲明)淹冰,否則編譯時會報錯:S0045: Attribute declared inside a function
二、attribute變量的應(yīng)用
了解了attribute修飾符的特性后巨柒,我們來學(xué)習(xí)如何使用它樱拴。
1. 獲取attribute變量的地址索引
我們可以通過glGetAttribLocation(int program, String name)
獲取一個attribute變量的地址索引。
int glGetAttribLocation(int program, String name)
program
:已鏈接的SL程序?qū)ο蟆?br>name
:attribute變量名洋满。
return
:返回attribute變量表中的位置晶乔。請注意,這個方法必須在
glLinkProgram(int program)
之后調(diào)用才有效牺勾,否則返回索引值為-1
正罢。另外,當(dāng)attribute變量未處于active
狀態(tài)(只聲明未使用或使用無意義驻民,這種情況的attribute變量在編譯時會被優(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
回还。
從這個結(jié)果我們可以看到裆泳,只聲明未使用的變量b
和使用無意義的變量f
返回的索引均為-1
。
另外柠硕,仔細(xì)觀察其他幾個變量的索引我們會發(fā)現(xiàn)它符合上面講到的第五條特性工禾。并且,如果地址索引在一定程度上反應(yīng)了變量在物理空間的相對位置蝗柔,那么我們可以大膽猜測闻葵,OpenGL ES/GLSL為了更好的利用硬件存儲空間,并沒有按照我們聲明的順序存儲變量癣丧,而是按地址空間大小從大到小存儲笙隙。此處,我們暫且不去討論OpenGL ES或者GLSL規(guī)定的存儲方式坎缭,留待以后再討論竟痰。
2. 手動設(shè)置attribute變量的地址索引
除了在鏈接時自動為attribute變量分配地址索引外,GLSL還允許我們在鏈接程序前通過glBindAttribLocation(int, int, String)
手動為attribute變量設(shè)置地址索引掏呼。
void glBindAttribLocation(int program, int index, String name)
program
:未鏈接的SL程序?qū)ο蟆?br>index
:想要的設(shè)置的attribute變量在attribute變量表中的位置坏快,即glGetAttribLocation(int program, String name)
的返回結(jié)果。它必須是一個0
到gl_MaxVertexAttribs-1
的值憎夷,否則此次方法調(diào)用無效莽鸿。
name
:attribute變量名。這個方法必須在
glLinkProgram(int program)
調(diào)用前使用,否則設(shè)置不會生效祥得。
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
在程序鏈接后綁定饮焦,此時attribute變量表已經(jīng)生成怕吴,改變無效。只有對變量a
的設(shè)置是有效的县踢,b
转绷、c
的地址索引依然是鏈接器分配的索引。
3. 修改attribute變量的值
OpenGL ES提供了兩個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 )
议经,其中前一個方法是從VertexBufferObject
為attribute變量賦值,后一個方法是從主內(nèi)存為attribute變量賦值谴返,前一個方法留到后面講爸业,此處我們主要了解后一個方法。
void glVertexAttribPointer(int indx, int size, int type, boolean normalized, int stride, java.nio.Buffer ptr)
index
:所要設(shè)置的變量的地址位置亏镰。由glGetAttribLocation(int program, String name)
得到。
size
:每個頂點所需要的元素個數(shù)拯爽。必須是1索抓,2,3毯炮,4中的一個值逼肯,默認(rèn)為4。
type
:表示數(shù)組中每個元素的數(shù)據(jù)類型桃煎。必須是GL_BYTE
篮幢,GL_UNSIGNED_BYTE
,GL_SHORT
为迈,GL_UNSIGNED_SHORT
三椿,GL_FIXED
或GL_FLOAT
中的一個,默認(rèn)為GL_FLOAT
葫辐。
normalized
:表示數(shù)據(jù)是否需要在傳入到頂點數(shù)組前進行歸一化處理搜锰。
stride
:表示ptr
中兩個連續(xù)元素之間的偏移字節(jié)數(shù)。如果stride
為0耿战,那么則表示ptr
中個元素是緊密貼合在一起的蛋叼。
ptr
:需要設(shè)置的數(shù)據(jù)的內(nèi)存緩存。為了最大限度提高效率,建議使用ByteBuffer
裝載數(shù)據(jù)狈涮。需要注意的是狐胎,不同存儲設(shè)備存取字節(jié)順序有兩種不同方式,分別是Big-Endian
(高位優(yōu)先)或Little-Endian
(低位優(yōu)先)歌馍,Android中是Little-Endian
握巢。因此,我們需要使用當(dāng)前運行設(shè)備上的存取字節(jié)順序骆姐,這可以通過ByteOrder.nativeOrder()
方法獲得镜粤。另外,在OpenGL訪問前還需要調(diào)用
glEnableVertexAttribArray(int)
方法啟用該通用頂點數(shù)組玻褪。否則glVertexAttribPointer()
調(diào)用無效肉渴,OpenGL會使用靜態(tài)頂點屬性值。
fun vertexAttribPointer(program: Int, name: String, size: Int, pType: Int,
normalized: Boolean, stride: Int, ptr: FloatArray): Int {
return glGetAttribLocation(program, name).also {
//為頂點屬性傳入數(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
}