基于OpenGL ES2.0 for Android。
一个程序需要有数据输入,否则没有实际意义,正如下面这段代码。
void main() {
gl_Position = vec4(0);
}
尽管这个着色器是“格式良好的”,但是在实际应用中,我们应避免这种“写死在程序里”的数据。那么,如何往GLSL着色器中写入数据呢?
与C语言一样,GLSL可以通过变量来接受应用程序输入的变量。但是GLSL中只有特定的修饰符修饰的变量才可以用于数据的输入输出。这些特定修饰符主要有attribute
,varying
,uniform
。
首先是attribute修饰符。按照官方文档介绍,attribute修饰的变量用于声明基于每个顶点从OpenGL ES传递到顶点着色器的变量。这句话如何理解呢?我们知道,顶点着色器定义了图形上顶点的位置,而各种图形中除了“点”只有一个顶点,其他图形都具有一个以上的顶点。attribute修饰的变量就会根据顶点的数量依序设置和获取顶点属性数组的值。attribute所修饰的变量通常用来输入顶点坐标、法线等数据。
一、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;
}
在着色器内部对attribute修饰的变量赋值或修改,顶点着色器在编译阶段报错: S0027: Cannot modify an input variable
。
- attribute修饰的变量具有数量和内存空间限制。
类似于其他语言变量,attribute同样有变量地址空间大小的限制。attribute变量通过一个很小的固定的存储空间传递数据,因此,GLSL规定了所有非矩阵类型都有一个确定的不大于4个float大小
(四个浮点数所需存储空间)的存储大小。矩阵类型变量的存储大小等于矩阵的行数乘以 4个float大小
。
相应地,由于总存储空间大小固定且每个变量大小固定,GLSL限制了attribute所修饰的变量的最大计数(请注意,计数≠数量)。最大计数的值由提供的固定存储空间的大小决定,但GLSL规定了此最大计数的最小值(GLSL规定此最大计数最小值为8)。这个最大计数是一个编译时就确定的常量:gl_MaxVertexAttribs
,我们可以通过OpenGL ES API获取这个常量参数的值。
GLSL是如何计数地呢?我们可以简单地理解为,最大计数的值等于总存储空间大小除以 4个float大小
。所有非矩阵变量都计数1,即使这个变量小于4个float大小
,矩阵类型计数为矩阵的行数。如:一个float
变量和一个vec4
变量具有同样的计数1,一个mat2
变量计数为2,一个mat4
变量计数为4。一个最大计数为8的OpenGL ES实现能够分别存放8个float
,四个mat2
,两个mat4
。因此,将4个不相干的浮点数变量打包放到一个vec4
变量中能够最大限度利用硬件存储空间。
另外,只声明未使用的变量不计数。
我们可以简单的理解为,attribute变量存储在一个由硬件厂商决定的固定大小的内存缓冲区。它的最小计量单位为vec4
。
通过OpenGL ES glGet* API可以获取内部常量参数,下面的代码获取的是gl_MaxVertexAttribs
(attribute修饰符修饰的变量的最大计数)的值。
val parameters = IntArray(1)
GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, parameters, 0)
Log.e("parameters", parameters.contentToString())
打印结果parameters: [16]
。表明此设备上的OpenGL ES内置的最大attribute变量计数为16。现在我们通过代码测试超过这个计数会发生什么。
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)
执行报错,L0004 Not able to fit attributes
。错误代码L0004,表示Linker(链接器)第四号错误。我们可以去官方文档查找一下它的详细介绍。
L0004: Too many attribute values.
正是attribute计数超过限制。
现在我们简单地修改一下void main()
函数,将" gl_Position = a*b*c*d*e;\n"
修改为" gl_Position = a*b*c*e;\n"
,去掉了参数d
的使用,将使用参数计数减少到13。执行结果表明一切正常。
- 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变量的应用
了解了attribute修饰符的特性后,我们来学习如何使用它。
1. 获取attribute变量的地址索引
我们可以通过glGetAttribLocation(int program, String name)
获取一个attribute变量的地址索引。
int glGetAttribLocation(int program, String name)
program
:已链接的SL程序对象。
name
:attribute变量名。
return
:返回attribute变量表中的位置。请注意,这个方法必须在
glLinkProgram(int program)
之后调用才有效,否则返回索引值为-1
。另外,当attribute变量未处于active
状态(只声明未使用或使用无意义,这种情况的attribute变量在编译时会被优化忽略),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)
打印结果locations: locationA: 0, locationB: -1, locationC: 8, locationD: 4, locationE: 9, locationF: -1
。
从这个结果我们可以看到,只声明未使用的变量b
和使用无意义的变量f
返回的索引均为-1
。
另外,仔细观察其他几个变量的索引我们会发现它符合上面讲到的第五条特性。并且,如果地址索引在一定程度上反应了变量在物理空间的相对位置,那么我们可以大胆猜测,OpenGL ES/GLSL为了更好的利用硬件存储空间,并没有按照我们声明的顺序存储变量,而是按地址空间大小从大到小存储。此处,我们暂且不去讨论OpenGL ES或者GLSL规定的存储方式,留待以后再讨论。
2. 手动设置attribute变量的地址索引
除了在链接时自动为attribute变量分配地址索引外,GLSL还允许我们在链接程序前通过glBindAttribLocation(int, int, String)
手动为attribute变量设置地址索引。
void glBindAttribLocation(int program, int index, String name)
program
:未链接的SL程序对象。
index
:想要的设置的attribute变量在attribute变量表中的位置,即glGetAttribLocation(int program, String name)
的返回结果。它必须是一个0
到gl_MaxVertexAttribs-1
的值,否则此次方法调用无效。
name
:attribute变量名。这个方法必须在
glLinkProgram(int program)
调用前使用,否则设置不会生效。
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)
打印结果locations: locationA: 2, locationB: 6, locationC: 10
。
可以看到,b
虽然在程序链接前绑定,但index
越界。c
在程序链接后绑定,此时attribute变量表已经生成,改变无效。只有对变量a
的设置是有效的,b
、c
的地址索引依然是链接器分配的索引。
3. 修改attribute变量的值
OpenGL ES提供了两个API为attribute变量设置变量值,它们分别是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变量赋值,后一个方法是从主内存为attribute变量赋值,前一个方法留到后面讲,此处我们主要了解后一个方法。
void glVertexAttribPointer(int indx, int size, int type, boolean normalized, int stride, java.nio.Buffer ptr)
index
:所要设置的变量的地址位置。由glGetAttribLocation(int program, String name)
得到。
size
:每个顶点所需要的元素个数。必须是1,2,3,4中的一个值,默认为4。
type
:表示数组中每个元素的数据类型。必须是GL_BYTE
,GL_UNSIGNED_BYTE
,GL_SHORT
,GL_UNSIGNED_SHORT
,GL_FIXED
或GL_FLOAT
中的一个,默认为GL_FLOAT
。
normalized
:表示数据是否需要在传入到顶点数组前进行归一化处理。
stride
:表示ptr
中两个连续元素之间的偏移字节数。如果stride
为0,那么则表示ptr
中个元素是紧密贴合在一起的。
ptr
:需要设置的数据的内存缓存。为了最大限度提高效率,建议使用ByteBuffer
装载数据。需要注意的是,不同存储设备存取字节顺序有两种不同方式,分别是Big-Endian
(高位优先)或Little-Endian
(低位优先),Android中是Little-Endian
。因此,我们需要使用当前运行设备上的存取字节顺序,这可以通过ByteOrder.nativeOrder()
方法获得。另外,在OpenGL访问前还需要调用
glEnableVertexAttribArray(int)
方法启用该通用顶点数组。否则glVertexAttribPointer()
调用无效,OpenGL会使用静态顶点属性值。
fun vertexAttribPointer(program: Int, name: String, size: Int, pType: Int,
normalized: Boolean, stride: Int, ptr: FloatArray): Int {
return glGetAttribLocation(program, name).also {
//为顶点属性传入数据
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
}