65.9K
CodeProject 正在变化。 阅读更多。
Home

Android 上的基本着色器编程

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2015年7月10日

CPOL

5分钟阅读

viewsIcon

36128

downloadIcon

675

开始在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应用程序可以使用NDKGLSurfaceView来使用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_SHADERGL_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()来传递数据。下面的代码说明了这一点。实例vertexBufferFloatBuffer类型。

// 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);
}

应用颜色反转着色器后的输出图像。
© . All rights reserved.