你的位置:首页 > 操作系统

[操作系统]OpenGL ES学习笔记(三)纹理


首先申明下,本文为笔者学习《OpenGL ES应用开发实践指南(Android卷)》的笔记,涉及的代码均出自原书,如有需要,请到原书指定源码地址下载。

      《OpenGL ES学习笔记(二)——平滑着色、自适应宽高及三维图像生成》中阐述的平滑着色、自适应宽高是为了实现在移动端模拟真实场景采用的方法,并且通过w分量增加了三维视角,在具体实现上采用了正交投影、透视投影的理论。本文将在此基础上,构建更加精美的三维场景。三维效果本质上是点、直线和三角形的组合,纹理是将图像或者照片覆盖到物体表面,形成精美的细节。在实现上具体分为两步:1)将纹理图片加载进OpenGL;2)OpenGL将其显示到物体表面。(有点像把大象装进冰箱分几步~~~)不过,在实现过程中,涉及到着色器程序的管理,涉及到不同的纹理过滤模式,涉及到顶点数据新的类结构等问题,下面将一一对其阐述:

  1. 纹理加载
  2. 纹理着色器
  3. 更新顶点数据类结构
  4. 着色器程序类
  5. 纹理绘制


一、纹理加载

      将纹理覆盖到物体表面,最终是通对齐坐标来实现的。而OpenGL中二维纹理的坐标与计算机图像的坐标并不一致,因此,首先对比下两者的不同。

clip_image001

      可见,两者的差别在于绕横轴翻转180度。另外,OpenGL ES支持的纹理不必是正方形,但每个维度都必须是2的幂。

      加载纹理图片的方法参数列表应该包括Android上下文(Context)和资源ID,返回值应该是OpenGL纹理的ID,因此,该方法申明如下:

public static int loadTexture(Context context, int resourceId) {}

      首先,创建一个纹理对象,与普通OpenGL对象生成模式一样。生成成功之后,申明纹理调用应该应用于这个纹理对象。其次,加载位图数据,OpenGL读入位图数据并复制到前面绑定的纹理对象。

final int[] textureObjectIds = new int[1];glGenTextures(1, textureObjectIds, 0);if (textureObjectIds[0] == 0) {  if (LoggerConfig.ON) {    Log.w(TAG, "Could not generate a new OpenGL texture object.");  }  return 0;}

final BitmapFactory.Options options = new BitmapFactory.Options();options.inScaled = false;// Read in the resourcefinal Bitmap bitmap = BitmapFactory.decodeResource(  context.getResources(), resourceId, options);  if (bitmap == null) {    if (LoggerConfig.ON) {      Log.w(TAG, "Resource ID " + resourceId + " could not be decoded.");    }    glDeleteTextures(1, textureObjectIds, 0);    return 0;  } // Bind to the texture in OpenGLglBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);

      这两段代码需要说明的并不多,其中options.inScaled = false表明OpenGL读入图像的非压缩形式的原始数据。OpenGL读入位图数据需要注意一点:纹理过滤。OpenGL纹理过滤模式如下表:(--内容来自原书)

GL_NEAREST

最近邻过滤

GL_NEAREST_MIPMAP_NEAREST

使用MIP贴图的最近邻过滤

GL_NEAREST_MIPMAP_LINEAR

使用MIP贴图级别之间插值的最近邻过滤

GL_LINEAR

双线性过滤

GL_LINEAR_MIPMAP_NEAREST

使用MIP贴图的双线性过滤

GL_LINEAR_MIPMAP_LINEAR

三线性过滤(使用MIP贴图级别之间插值的双线性过滤)

      至于每种过滤具体的解释及实现,请自行Google吧。这里对于缩小情况,采用了GL_LINEAR_MIPMAP_LINEAR,对于放大情况,采用了GL_LINEAR。

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

      加载纹理的最后一步就是将bitmap复制到当前绑定的纹理对象:

texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);

      绑定之后,仍然需要做一些后续操作,比如回收bitmap对象(bitmap内存占用大户),生成MIP贴图,接触纹理绑定,最后返回纹理对象ID。

glGenerateMipmap(GL_TEXTURE_2D);// Recycle the bitmap, since its data has been loaded into OpenGL.bitmap.recycle();// Unbind from the texture.glBindTexture(GL_TEXTURE_2D, 0);return textureObjectIds[0];

 

二、纹理着色器

      在继续采用GLSL编写着色器程序之前,先说明下之前遗漏的一个问题:

OpenGL着色语言(OpenGL Shading Language)是用来在OpenGL中着色编程的语言,也即开发人员写的短小的自定义程序,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程型。比如:视图转换、投影转换等。

GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片断着色器),有时还会有Geometry Shader(几何着色器)。负责运行顶点着色的是顶点着色器。它可以得到当前OpenGL 中的状态,GLSL内置变量进行传递。GLSL其使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。

      这段内容来自百度百科,有一点需要重视:采用GLSL编写的程序是在GPU中执行的,意味着着色器程序并不占用CPU时间,这启发我们在某些耗时的渲染程序(摄像头实时滤镜)中可以采用GLSL实现,或许比NDK方式实现数据处理更为高效。后续笔者会在这方面实践,这里先说明纹理着色器程序。同样,为了支持纹理,需对顶点着色器和片段着色器进行更改。

uniform mat4 u_Matrix;attribute vec4 a_Position; attribute vec2 a_TextureCoordinates;varying vec2 v_TextureCoordinates;void main()          {                v_TextureCoordinates = a_TextureCoordinates;        gl_Position = u_Matrix * a_Position;  }

precision mediump float;              uniform sampler2D u_TextureUnit;                      varying vec2 v_TextureCoordinates;                        void main()              {                   gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);                  }

      上述顶点着色器中,变量a_TextureCoordinates的类型为vec2,因为纹理坐标的两个分量:S坐标和T坐标。片段着色器中,sampler2D类型的u_TextureUnit表示接收二维纹理数据的数组。

 

三、更新顶点数据类结构

      首先将不同类型的顶点数据分配到不同的类中,每个类代表一个物理对象的类型。在类的构造器中初始化VertexArray对象,VertexArray的实现与前述文章中描述的一致,采用FloatBuffer在本地代码中存储顶点矩阵数据,并创建通用方法将着色器的属性与顶点数据关联。

private final FloatBuffer floatBuffer;public VertexArray(float[] vertexData) {    floatBuffer = ByteBuffer      .allocateDirect(vertexData.length * BYTES_PER_FLOAT)      .order(ByteOrder.nativeOrder())      .asFloatBuffer()      .put(vertexData);}    public void setVertexAttribPointer(int dataOffset, int attributeLocation,    int componentCount, int stride) {        floatBuffer.position(dataOffset);        glVertexAttribPointer(attributeLocation, componentCount, GL_FLOAT,       false, stride, floatBuffer);    glEnableVertexAttribArray(attributeLocation);        floatBuffer.position(0);}

public Table() {  vertexArray = new VertexArray(VERTEX_DATA);}

      构造器中传入的参数VERTEX_DATA就是顶点数据。

private static final float[] VERTEX_DATA = {    // Order of coordinates: X, Y, S, T    // Triangle Fan      0f,  0f, 0.5f, 0.5f,     -0.5f, -0.8f,  0f, 0.9f,      0.5f, -0.8f,  1f, 0.9f,     0.5f, 0.8f,  1f, 0.1f,     -0.5f, 0.8f,  0f, 0.1f,     -0.5f, -0.8f,  0f, 0.9f };

      在该组数据中,x=0,y=0对应纹理S=0.5,T=0.5,x=-0.5,y=-0.8对应纹理S=0,T=0.9,之所以有这种对应关系,看下前面讲到的OpenGL纹理坐标与计算机图像坐标的对比就清楚啦。至于纹理部分的数据使用了0.1和0.9作为T坐标,是为了避免把纹理压扁,而对纹理进行了裁剪,截取了0.1到0.9的部分。

      初始化vertexArray之后,通过其setVertexAttribPointer()方法将顶点数据绑定到着色器程序上。

public void bindData(TextureShaderProgram textureProgram) {  vertexArray.setVertexAttribPointer(    0,     textureProgram.getPositionAttributeLocation(),     POSITION_COMPONENT_COUNT,    STRIDE);      vertexArray.setVertexAttribPointer(    POSITION_COMPONENT_COUNT,     textureProgram.getTextureCoordinatesAttributeLocation(),    TEXTURE_COORDINATES_COMPONENT_COUNT,     STRIDE);}

      这个方法为每个顶点调用了setVertexAttribPointer(),并从着色器程序获取每个属性的位置。通过getPositionAttributeLocation()把位置数据绑定到被引用的着色器属性上,并通过getTextureCoordinatesAttributeLocation()把纹理坐标数据绑定到被引用的着色器属性。

      完成上述绑定以后,绘制只需要调用glDrawArrays()实现。

public void draw() {                  glDrawArrays(GL_TRIANGLE_FAN, 0, 6);}

 

四、着色器程序类

      随着纹理的使用,着色器程序变得更多,因此需要为着色器程序添加管理类。根据着色器分类,这里分别创建纹理着色器类和颜色着色器类,且抽象它们的共同点,形成基类ShaderProgram,TextureShaderProgram和ColorShaderProgram分别继承于此实现。ShaderProgram主要的功能就是根据Android上下文Context和着色器资源ID读入着色器程序,其构造器参数列表如下:

protected ShaderProgram(Context context, int vertexShaderResourceId,    int fragmentShaderResourceId) {  ……}

      读入着色器程序的实现应该在ShaderHelper类中,其步骤与之前所述相似,包括编译、链接等步骤。

public static int buildProgram(String vertexShaderSource,    String fragmentShaderSource) {  int program;  // Compile the shaders.  int vertexShader = compileVertexShader(vertexShaderSource);  int fragmentShader = compileFragmentShader(fragmentShaderSource);  // Link them into a shader program.  program = linkProgram(vertexShader, fragmentShader);  if (LoggerConfig.ON) {    validateProgram(program);  }  return program;}

      compileVertexShader(编译)和linkProgram(链接)的实现在之前的笔记中已详细描述过。ShaderProgram的构造器调用上述buildProgram()方法即可。

program = ShaderHelper.buildProgram(  TextResourceReader.readTextFileFromResource(    context, vertexShaderResourceId),  TextResourceReader.readTextFileFromResource(    context, fragmentShaderResourceId));

      得到着色器程序之后,定义OpenGL后续的渲染使用该程序。

public void useProgram() {  // Set the current OpenGL shader program to this program.  glUseProgram(program);}

      着色器程序类TextureShaderProgram和ColorShaderProgram在构造器中调用父类的构造函数,并读入纹理着色器中uniform和属性的位置。

public TextureShaderProgram(Context context) {  super(context, R.raw.texture_vertex_shader,    R.raw.texture_fragment_shader);  // Retrieve uniform locations for the shader program.  uMatrixLocation = glGetUniformLocation(program, U_MATRIX);  uTextureUnitLocation = glGetUniformLocation(program, U_TEXTURE_UNIT);      // Retrieve attribute locations for the shader program.  aPositionLocation = glGetAttribLocation(program, A_POSITION);  aTextureCoordinatesLocation =     glGetAttribLocation(program, A_TEXTURE_COORDINATES);}

      接下来,传递矩阵给uniform,这在之前的笔记中描述过了。

// Pass the matrix into the shader program.glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);

      纹理的传递相对于矩阵的传递要复杂一些,因为纹理并不直接传递,而是采用纹理单元(Texture Unit)来保存,因为一个GPU只能同时绘制数量有限的纹理,使用这些纹理单元表示正在被绘制的活动的纹理。

// Set the active texture unit to texture unit 0.glActiveTexture(GL_TEXTURE0);// Bind the texture to this unit.glBindTexture(GL_TEXTURE_2D, textureId);// Tell the texture uniform sampler to use this texture in the shader by// telling it to read from texture unit 0.glUniform1i(uTextureUnitLocation, 0);

      glActiveTexture(GL_TEXTURE0)表示把活动的纹理单元设置为纹理单元0,调用glBindTexture将textureId指向的纹理绑定到纹理单元0,最后,调用glUniform1i把选定的纹理单元传递给片段着色器中的u_TextureUnit(sampler2D)。

      颜色着色器类与纹理着色器类的实现基本类似,同样在构造器中获取uniform和属性的位置,不过设置uniform值只需传递矩阵即可。

public void setUniforms(float[] matrix) {  // Pass the matrix into the shader program.  glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);}

 

五、纹理绘制

      通过前面的准备,顶点数据,着色器程序已经放到了不同的类中,因此,在渲染类中可以通过前面的实现进行纹理绘制了。AirHockeyRenderer类更新后的成员变量和构造函数如下:

private final Context context;private final float[] projectionMatrix = new float[16];private final float[] modelMatrix = new float[16];private Table table;private Mallet mallet;  private TextureShaderProgram textureProgram;private ColorShaderProgram colorProgram;    private int texture;public AirHockeyRenderer(Context context) {  this.context = context;}

      初始化变量主要包括清理屏幕、初始化顶点数组和着色器程序,加载纹理等。

@Overridepublic void onSurfaceCreated(GL10 glUnused, EGLConfig config) {  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);  table = new Table();  mallet = new Mallet();      textureProgram = new TextureShaderProgram(context);  colorProgram = new ColorShaderProgram(context);          texture = TextureHelper.loadTexture(context, R.drawable.air_hockey_surface);}

      最后,在onDrawFrame()中绘制物体,绘制的方法就是通过调用前面着色器类和物体类(顶点数据)的方法来实现的。

@Overridepublic void onDrawFrame(GL10 glUnused) {  // Clear the rendering surface.  glClear(GL_COLOR_BUFFER_BIT);  // Draw the table.  textureProgram.useProgram();  textureProgram.setUniforms(projectionMatrix, texture);  table.bindData(textureProgram);  table.draw();  // Draw the mallets.  colorProgram.useProgram();  colorProgram.setUniforms(projectionMatrix);  mallet.bindData(colorProgram);  mallet.draw();}

 

总结一下,这篇笔记涉及到一下内容:

1)加载纹理并显示到物体上;

2)重新组织程序,管理多个着色器和顶点数据之间的切换;

3)调整纹理以适应它们将要被绘制的形状,既可以调整纹理坐标,也可以通过拉伸或压扁纹理本身来实现;

4)纹理不能直接传递,需要被绑定到纹理单元,然后将纹理单元传递给着色器;