Android OpenGLES2.0开发(七):纹理贴图之显示图片

249 阅读7分钟

人生要三见:见天地,见众生,见自己。

通过前面的篇章我们熟悉了OpenGL ES绘制的基本流程了,我们主要就是绘制了一些基本的图形,而OpenGL的能力远不止如此。他可以构建绚丽多彩的游戏世界,多种建筑风格、逼真的人物皮肤等等,设计师将各种效果的图做好给到工程师然后用OpenGL ES绘制到模型上去,而这个过程我们称之为纹理贴图

一. 什么是纹理贴图

纹理贴图是将图像信息映射到三角形网格上的技术,以此来增加物体表面的细节,令物体更具有真实感。

请添加图片描述

二. 纹理映射原理

如何将一幅纹理图映射到相应的几何图元呢?必须告诉GPU如何进行纹理映射,也就是为图元的顶点指定恰当的纹理坐标。纹理坐标用浮点数来表示,范围一般从0.0到1.0,左下角坐标为(0.0,0.0),右上角坐标为(1.0,1.0),左上角坐标为(0.0,1.0),右下角坐标为(1.0,0.0)。

纹理坐标系

纹理坐标.jpeg

OpenGL ES坐标系

为了将纹理正确通过OpenGL ES绘制到图元上,我们必须将纹理坐标点和OpenGL ES坐标点一一对应起来,及左上对左上 (0.0,1.0) ->(-1.0,1.0),左下对左下 (0.0,0.0)->(-1.0,-1.0) 等等。

三. 显示图片

根据纹理映射原理,以及我们之前绘制正方形的的经验,我们可以根据下面的步骤绘制一张图片。

首先我们将绘制正方形的类Square.java复制一份改名为Image.java,接下来的操作我们都在Image类中进行:

1. 修改纹理着色器

首先,我们需要修改我们的着色器,将顶点着色器修改为:

// 顶点着色器代码
private final String vertexShaderCode =
        "uniform mat4 uMVPMatrix;\n" +
                // 顶点坐标
                "attribute vec4 vPosition;\n" +
                // 纹理坐标
                "attribute vec2 vTexCoordinate;\n" +
                
                "varying vec2 aTexCoordinate;\n" +
                
                "void main() {\n" +
                "  gl_Position = uMVPMatrix * vPosition;\n" +
                "  aTexCoordinate = vTexCoordinate;\n" +
                "}\n";

可以看到,顶点着色器中增加了一个vec2变量,并将这个变量传递给了片元着色器,这个变量就是纹理坐标。接着我们修改片元着色器为:

// 片段着色器代码
private final String fragmentShaderCode =
        "precision mediump float;\n" +
                "uniform sampler2D vTexture;\n" +
                "varying vec2 aTexCoordinate;\n" +
                "void main() {\n" +
                "  gl_FragColor = texture2D(vTexture, aTexCoordinate);\n" +
                "}\n";

片元着色器中,增加了一个sampler2D的变量,sampler2D我们在前一篇博客GLSL语言基础中提到过,是GLSL的变量类型之一的取样器。texture2D也有提到,它是GLSL的内置函数,用于2D纹理取样,根据纹理取样器和纹理坐标,可以得到当前纹理取样得到的像素颜色。

2. 设置顶点坐标和纹理坐标

根据纹理映射原理中的介绍,我们将顶点坐标设置为:

/**
 * 顶点坐标数组
 * 顶点坐标系中原点(0,0)在画布中心
 * 向左为x轴正方向
 * 向上为y轴正方向
 * 画布四个角坐标如下:
 * (-1, 1),(1, 1)
 * (-1,-1),(1,-1)
 */
private float vertexCoords[] = {
        -1.0f, 1.0f,   // 左上
        -1.0f, -1.0f,  // 左下
        1.0f, 1.0f,    // 右上
        1.0f, -1.0f,   // 右下
};

相应的,对照顶点坐标,我们可以设置纹理坐标为:

/**
 * 纹理坐标数组
 * 这里我们需要注意纹理坐标系,原点(0,0s)在画布左下角
 * 向左为x轴正方向
 * 向上为y轴正方向
 * 画布四个角坐标如下:
 * (0,1),(1,1)
 * (0,0),(1,0)
 */
private float textureCoords[] = {
        0.0f, 1.0f, // 左上
        0.0f, 0.0f, // 左下
        1.0f, 1.0f, // 右上
        1.0f, 0.0f, // 右下
};

3. 初始化

构造顶点缓冲区,并在构造函数传入需要显示的Bitmap

// 顶点坐标缓冲区
private FloatBuffer vertexBuffer;

// 纹理坐标缓冲区
private FloatBuffer textureBuffer;

// 此数组中每个顶点的坐标数
static final int COORDS_PER_VERTEX = 2;

public Image(Bitmap bitmap) {
    mBitmap = bitmap;

    // 初始化形状坐标的顶点字节缓冲区
    ...

    // 初始化纹理坐标顶点字节缓冲区
    textureBuffer = ByteBuffer.allocateDirect(textureCoords.length * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(textureCoords);
    textureBuffer.position(0);
}

3. 计算变换矩阵

我们需要在surfaceChanged中根据Bitmap的宽高修改变换矩阵

public void surfaceChanged(int width, int height) {
    GLES20.glViewport(0, 0, width, height);

    int w = mBitmap.getWidth();
    int h = mBitmap.getHeight();
    float sWH = w / (float) h;
    float sWidthHeight = width / (float) height;
    if (width > height) {
        if (sWH > sWidthHeight) {
            Matrix.orthoM(mProjectionMatrix, 0, -sWidthHeight * sWH, sWidthHeight * sWH, -1, 1, 3, 7);
        } else {
            Matrix.orthoM(mProjectionMatrix, 0, -sWidthHeight / sWH, sWidthHeight / sWH, -1, 1, 3, 7);
        }
    } else {
        if (sWH > sWidthHeight) {
            Matrix.orthoM(mProjectionMatrix, 0, -1, 1, -1 / sWidthHeight * sWH, 1 / sWidthHeight * sWH, 3, 7);
        } else {
            Matrix.orthoM(mProjectionMatrix, 0, -1, 1, -sWH / sWidthHeight, sWH / sWidthHeight, 3, 7);
        }
    }
    //设置相机位置
    Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
    //计算变换矩阵
    Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
}

4. 绘制图片

接下里我们就可以进行我们的绘制操作,由于我们修改了我们着色器程序,那么绘制前我们需要向着色器传入我们的纹理坐标和纹理对象

传入纹理坐标

public void surfaceCreated() {
	...

    // 获取顶点着色器中纹理坐标的句柄
    texCoordinateHandle = GLES20.glGetAttribLocation(mProgram, "vTexCoordinate");
}

public void draw() {
    // 将程序添加到OpenGL ES环境
    GLES20.glUseProgram(mProgram);

    // 重新绘制背景色为黑色
    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

    // 为正方形顶点启用控制句柄
    ...

    // 启用纹理坐标控制句柄
    GLES20.glEnableVertexAttribArray(texCoordinateHandle);
    // 写入坐标数据
    GLES20.glVertexAttribPointer(texCoordinateHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureBuffer);
    ...
}

传入纹理对象

public void surfaceCreated() {
	...
    // 创建纹理句柄
    textureId = createTexture();
}

public void draw() {
    ...

    // 激活纹理编号0
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    // 绑定纹理
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
    // 设置纹理采样器编号,该编号和glActiveTexture中设置的编号相同
    GLES20.glUniform1i(texHandle, 0);

    // 绘制
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

    // 禁用顶点阵列
    GLES20.glDisableVertexAttribArray(positionHandle);
    GLES20.glDisableVertexAttribArray(texCoordinateHandle);
}

private int createTexture() {
    Log.i(TAG, "Bitmap:" + mBitmap);
    int[] texture = new int[1];
    if (mBitmap != null && !mBitmap.isRecycled()) {
        //生成纹理
        GLES20.glGenTextures(1, texture, 0);
        //生成纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);
        //设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        //设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        //设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        //设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
        //根据以上指定的参数,生成一个2D纹理
        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
        // 取消绑定纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

        return texture[0];
    }
    return 0;
}

Image添加到GLSurfaceView中显示如下:

Screenshot_20241025-105602.jpg

what's fuck! 怎么会显示倒了呢???

四. 纹理显示坑

查阅了很多资料,开始以为纹理坐标写错了,而有些博客纹理坐标中左上角是 (0.0,0.0),但是可以肯定的告诉你纹理坐标系左下角一定是 (0.0,0.0),右上角是 (1.0,1.0)

倒置的本质原因:是因为Android中Bitmap生成纹理时,数据是从Bitmap左上角开始拷贝到纹理坐标 (0.0,0.0),这样就会导致图片显示上下翻转180度

解决1: 修改纹理坐标,上下翻转,该方案也是众多博文使用的,但是他们解释是错误的。这种错误的解释会导致Camera预览映射的错乱,在FBO绘制时纹理坐标又转回来

private float textureCoords[] = {
        0.0f, 0.0f, // 左上
        0.0f, 1.0f, // 左下
        1.0f, 0.0f, // 右上
        1.0f, 1.0f, // 右下
};

解决2: 修改矩阵上下翻转

public void surfaceChanged(int width, int height) {
	...
    /**
     * 由于Bitmap拷贝到纹理中,数据从Bitmap左上角开始拷贝到纹理的原点(0,0)
     * 导致图像上下翻转了180度,所以绘制坐标需要上下翻转180度才行
     */
    Matrix.scaleM(mMVPMatrix, 0, 1, -1, 1);
}

通过以上解决方案,任选其一:

Screenshot_20241025-112715.jpg

我个人倾向于解决方案二,对于初学者来说对纹理坐标系OpenGL ES坐标系必须要有基本的认识,防止今后既要变换顶点坐标,又要变换矩阵时的概念混乱。所以为了控制变量,我们坐标系永远都定死,要变换就变换矩阵。如果你已经是大拿了,各种原理都了然于胸,那么任何招式皆可用,甚至可以创造招式。

最后

本章我们介绍了使用OpenGL ES进行纹理贴图,了解了纹理在OpenGL ES的作用及用法。学了这么多章节后,我们终于不用再绘制简单的图形了,也能绘制一些有趣的东西了。

github地址:github.com/xiaozhi003/…