Android 上的基本着色器编程





5.00/5 (1投票)
开始在Android上进行OpenGL ES 2.0着色器编程
在Android中初始化OpenGL ES 2.0
着色器是GPU上执行的小型程序。着色器自OpenGL ES 2.0起可用,Android自Android SDK API级别8(Froyo)起增加了对OpenGL ES 2.0的支持。Android应用程序可以使用NDK或GLSurfaceView
来使用OpenGL ES 2.0。后者更容易。本文将仅使用GLSurfaceView
。如何在Android中显示OpenGL ES 2.0图形与OpenGL ES 1.0类似,即使用GLSurfaceView
并实现GLSurfaceView.Renderer
接口。不同之处在于调用setEGLContextClientVersion()
方法时使用的值。您需要使用2来指示您要使用OpenGL ES 2.0。下面的代码显示了如何初始化GLSurfaceView
以使用OpenGL ES 2.0。请注意,GLRenderer
类是我们自己的实现了GLSurfaceView.Renderer
接口的类。
renderer=new GLRenderer(this);
//load shader from resource
renderer.setVertexShader(
ResourceHelper.readRawTextFile(this,
R.raw._vertex_shader));
renderer.setFragmentShader(
ResourceHelper.readRawTextFile(this,
R.raw._fragment_shader));
//load texture from resource
renderer.setResTexId(R.drawable.kayla);
GLSurfaceView view=new GLSurfaceView(this);
//only OpenGL ES 2.0
view.setEGLContextClientVersion(2);
view.setRenderer(renderer);
要使用OpenGL ES 2.0功能,您可以使用GLES20
。例如,要调用glClear()
,您可以使用类似这样的代码
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
创建着色器资源
在使用着色器之前,必须通过调用glCreateShader()
来创建着色器资源。它需要一个整数参数,即您需要创建的着色器类型。您可以使用GL_VERTEX_SHADER
或GL_FRAGMENT_SHADER
来分别创建顶点着色器或片段着色器。
int vtxshaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
int frgshaderHandle = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
glCreateShader()
返回已创建的着色器资源的句柄。要删除着色器资源,您需要调用glDeleteShader()
并将此句柄传递进去。
着色器编译
在OpenGL ES 2.0着色器编程中,有两个与GPU上运行的代码相关的概念:着色器和程序。这些概念类似于C语言中的编译和链接过程。在编译阶段,着色器源代码被翻译成中间代码。之后在链接阶段,它被链接成一个可供GPU运行的程序。
要编译着色器代码,您需要通过调用glShaderSource()
来提供要编译的着色器代码(显而易见),它需要两个参数:着色器资源的句柄和要编译的着色器源代码。要启动编译过程,请调用glCompileShader()
并将着色器资源句柄传递进去。
GLES20.glShaderSource(shaderHandle, shaderSourceCode);
GLES20.glCompileShader(shaderHandle);
人会犯错误。着色器编译可能成功也可能失败。要找出原因,您需要调用glGetShaderiv()
,它需要4个参数:着色器资源句柄、所需信息的类型(对于编译状态,我们使用GL_COMPILE_STATUS
)、用于填充状态的整数变量数组以及偏移量。
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == GLES20.GL_FALSE)
{
// failed.
} else {
// succeed.
}
glGetShaderiv()
仅返回状态。要了解导致编译失败的原因,您可以调用glGetShaderInfoLog()
。它返回一个String
,其中包含更具描述性的错误消息。
以下代码片段显示了编译着色器源代码的步骤。
public static int compileShader(final int shaderType,
String shaderSource)
{
int shaderHandle=GLES20.glCreateShader(shaderType);
if (shaderHandle !=0)
{
GLES20.glShaderSource(shaderHandle, shaderSource);
GLES20.glCompileShader(shaderHandle);
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == GLES20.GL_FALSE)
{
Log.e(TAG, "Error compiling shader: " +
GLES20.glGetShaderInfoLog(shaderHandle));
GLES20.glDeleteShader(shaderHandle);
shaderHandle = 0;
}
}
return shaderHandle;
}
就这样。现在着色器已准备好链接到程序中。
创建程序资源
要开始链接过程,您需要通过调用glCreateProgram()
来创建一个程序,它将返回程序句柄。要删除它,请将此句柄传递给glDeleteProgram()
。
int programHandle=GLES20.glCreateProgram();
GLES20.glDeleteProgram(programHandle);
将着色器附加到程序
每个需要链接的着色器必须首先附加到程序。我们调用glAttachShader()
来完成此操作,并传递程序句柄和着色器资源句柄。每个程序必须附加一个顶点着色器和一个片段着色器。
GLES20.glAttachShader(programHandle, vtxShader);
GLES20.glAttachShader(programHandle, pxlShader);
只要两个句柄都有效,可以在链接程序之前的任何时间附加着色器。例如,即使着色器尚未编译,也可以附加它。要从程序中分离着色器,请调用glDetachShader()
并传递程序句柄和着色器资源句柄。
GLES20.glDetachShader(programHandle, vtxShader);
将属性绑定到程序
如果您的着色器代码包含属性定义,您可以在链接之前使用glBindAttribLocation()
进行指定。例如,如果您的顶点着色器代码包含以下属性声明
attribute vec4 a_Position; // Per-vertex position information we will pass in.
要将a_Position
绑定到索引0
,我们可以这样调用它
GLES20.glBindAttribLocation(programHandle, 0, "a_Position");
链接程序
在着色器附加到程序之后,要启动链接过程,请调用glLinkProgram()
。链接过程会做几件事,例如确保输出程序代码不违反任何GPU要求(每个GPU对可供着色器使用的属性、统一变量和指令的数量都有限制),并输出可由GPU运行的机器码。
GLES20.glLinkProgram(programHandle);
可以通过调用glGetProgramiv()
并传递GL_LINK_STATUS
来查询链接状态,这与我们调用glGetShaderiv()
的方式类似。
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0]==GLES20.GL_FALSE)
{
//linking failed
} else {
//linking succeed
}
要获取有关导致链接失败的更多信息,请调用glGetProgramInfoLog()
。以下代码片段显示了创建程序的步骤。
public static int createProgram(final int vtxShader, final int pxlShader,
final String[] attributes)
{
int programHandle=GLES20.glCreateProgram();
if (programHandle!=0)
{
GLES20.glAttachShader(programHandle, vtxShader);
GLES20.glAttachShader(programHandle, pxlShader);
// Bind attributes
if (attributes != null)
{
final int size = attributes.length;
for (int i = 0; i < size; i++)
{
GLES20.glBindAttribLocation(
programHandle, i,
attributes[i]);
}
}
GLES20.glLinkProgram(programHandle);
//get linking status
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0]==0)
{
Log.e(TAG, "Error linking program: " +
GLES20.glGetProgramInfoLog(programHandle));
GLES20.glDeleteProgram(programHandle);
programHandle = 0;
}
}
return programHandle;
}
将数据复制到程序
在将数据复制到程序之前,请确保通过调用glUseProgram()
并传递要激活的程序句柄来激活程序。要将数据复制到统一变量或属性变量中,您需要使用glGetUniformLocation()
而不是之前绑定到程序的glGetAttribLocation()
来获取该统一变量或属性位置的句柄。第一个参数是程序句柄,第二个参数是在着色器源代码中写入的变量的字符串名称。这两种方法都返回变量位置的句柄。
GLES20.glUseProgram(g_hShader);
// get handle to variables
g_matMVPHandle = GLES20.glGetUniformLocation(g_hShader, "u_MVPMatrix");
g_texHandle = GLES20.glGetUniformLocation(g_hShader, "u_Texture");
g_timeHandle = GLES20.glGetUniformLocation(g_hShader, "elapsed_time");
g_posHandle = GLES20.glGetAttribLocation(g_hShader, "a_Position");
g_texCoordHandle = GLES20.glGetAttribLocation(g_hShader, "a_TexCoordinate");
稍后可以使用此句柄通过调用glVertexAttribPointer()
和glEnableVertexAttribArray()
来传递数据。下面的代码说明了这一点。实例vertexBuffer
是FloatBuffer
类型。
// Pass in the position information
vertexBuffer.position(0);
GLES20.glVertexAttribPointer(g_posHandle,
3, //number of coordinate per vertex
GLES20.GL_FLOAT,
false,
0, vertexBuffer);
GLES20.glEnableVertexAttribArray(g_posHandle);
绘制
在填充了顶点缓冲区、设置纹理以及模型、视图、投影变换之后,我们需要使用glDrawElements()
绘制一个四边形。我们使用2个三角形来绘制一个四边形,每个三角形由3个索引组成。
GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_SHORT, indexBuffer);
在Android虚拟设备上运行着色器
Android虚拟设备中的OpenGL ES 2.0支持随Android API级别15的发布而可用。要在Android模拟器中测试着色器代码,您必须创建一个最低目标API级别为15的Android虚拟设备(AVD),并选择使用主机GPU以启用GPU仿真,否则您的应用程序可能会崩溃。
示例演示
示例演示是一个Android应用程序,它使用着色器进行简单的图像处理。它通过加载图像并将其映射到矩形平面(四边形)上的纹理来实现。顶点着色器用于显示四边形并执行简单的模型、视图、投影变换。图像处理在片段着色器中完成。每当用户更改活动的片段着色器时,应用程序都会重新编译并将其链接到程序,然后显示输出。
下面的代码是顶点着色器
uniform mat4 u_MVPMatrix; //model/view/projection matrix.
attribute vec4 a_Position; //Per-vertex position information we will pass in.
attribute vec2 a_TexCoordinate;// Per-vertex texture coordinate information we will pass in.
varying vec2 v_TexCoordinate; //This will be passed into the fragment shader.
void main()
{
// Pass through the texture coordinate.
v_TexCoordinate = a_TexCoordinate;
// multiply the vertex by the matrix to get the normalized screen coordinates.
gl_Position = u_MVPMatrix * a_Position;
}
原始片段着色器执行简单的纹理查找并输出最终颜色。
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;
void main()
{
gl_FragColor = (texture2D(u_Texture, v_TexCoordinate));
}
模糊片段着色器对当前纹理坐标及其邻居进行多次纹理查找。它计算平均值然后输出最终颜色。
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordinate;
void main()
{
vec3 irgb = texture2D(u_Texture, v_TexCoordinate).rgb;
float ResS = 150.0;
float ResT = 150.0;
vec2 stp0 = vec2(1.0/ResS, 0.0);
vec2 st0p = vec2(0.0, 1.0/ResT);
vec2 stpp = vec2(1.0/ResS, 1.0/ResT);
vec2 stpm = vec2(1.0/ResS, -1.0/ResT);
vec3 i00 = texture2D(u_Texture, v_TexCoordinate).rgb;
vec3 im1m1 = texture2D(u_Texture, v_TexCoordinate-stpp).rgb;
vec3 ip1p1 = texture2D(u_Texture, v_TexCoordinate+stpp).rgb;
vec3 im1p1 = texture2D(u_Texture, v_TexCoordinate-stpm).rgb;
vec3 ip1m1 = texture2D(u_Texture, v_TexCoordinate+stpm).rgb;
vec3 im10 = texture2D(u_Texture, v_TexCoordinate-stp0).rgb;
vec3 ip10 = texture2D(u_Texture, v_TexCoordinate+stp0).rgb;
vec3 i0m1 = texture2D(u_Texture, v_TexCoordinate-st0p).rgb;
vec3 i0p1 = texture2D(u_Texture, v_TexCoordinate+st0p).rgb;
vec3 target = vec3(0.0, 0.0, 0.0);
target += 1.0*(im1m1+ip1m1+ip1p1+im1p1);
target += 2.0*(im10+ip10+i0p1);
target += 4.0*(i00);
target /= 16.0;
gl_FragColor = vec4(target, 1.0);
}
颜色反转着色器执行纹理查找并反转其颜色。
precision mediump float; // Set the default precision to medium. We don't need as high of a
// precision in the fragment shader.
uniform sampler2D u_Texture; // The input texture.
varying vec2 v_TexCoordinate; // Interpolated texture coordinate per fragment
void main()
{
gl_FragColor = 1.0-texture2D(u_Texture, v_TexCoordinate);
}