OpenGL ES学习笔记(三)——纹理

此生再无相见时 提交于 2020-02-28 12:40:25

首先申明下,本文为笔者学习《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 resource
final 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 OpenGL
glBindTexture(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;
}

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

@Override
public 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()中绘制物体,绘制的方法就是通过调用前面着色器类和物体类(顶点数据)的方法来实现的。

@Override
public 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)纹理不能直接传递,需要被绑定到纹理单元,然后将纹理单元传递给着色器;

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!