Android OpenGL基础(三、绘制Bitmap纹理)

4,284 阅读4分钟

一、纹理简介

  在Android OpenGL基础(一、绘制三角形四边形)一文中,我们简单介绍了如何绘制纯色三角形和四边形。现在介绍如何把一张图片贴到四边形上。
  在OpenGL中,我们把需要贴合到物体上的图片称为纹理。纹理是一个2D图片(甚至也有1D和3D的纹理),可以把纹理理解为一个细节更丰富的颜色的集合。与之前例子中纯色四边形不同的是,纹理细节更加丰富,OpenGL可以根据纹理计算得到四边形每个点应该绘制的颜色。

1.1 纹理坐标系

  为了能够把纹理映射到四边形上,我们需要指定四边形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样。在图形的其它片段上进行片段插值(Fragment Interpolation)。
  2D纹理坐标系与Android视图坐标系类似,左上角为(0,0)点,范围为0到1之间,两个坐标系分别称为X坐标(S坐标)、Y坐标(T坐标),如下所示:

image.png   程序中所要做的就是给四边形的顶点指定四个顶点对应哪个纹理坐标点,之后OpenGL会对四边形的其他部分进行片段插值。

1.2 创建纹理

  OpenGL创建一个纹理需要经过以下几步:

  1. 生成纹理;
  2. 绑定纹理;
  3. 设置纹理属性;纹理属性在第三、四小节再介绍;
  4. 把bitmap加载到纹理中。代码如下所示:
public class OpenGLUtils {
    /**
     * 根据bitmap创建2D纹理
     * @param bitmap
     * @param minFilter     缩小过滤类型 (1.GL_NEAREST ; 2.GL_LINEAR)
     * @param magFilter     放大过滤类型
     * @param wrapS         纹理S方向边缘环绕;也称作X方向
     * @param wrapT         纹理T方向边缘环绕;也称作Y方向
     * @return 返回创建的 Texture ID
     */
    public static int createTexture(Bitmap bitmap, int minFilter, int magFilter, int wrapS, int wrapT) {
        int[] textureHandle = createTextures(GLES20.GL_TEXTURE_2D, 1, minFilter, magFilter, wrapS, wrapT);
        if (bitmap != null) {
            // 4.把bitmap加载到纹理中
            android.opengl.GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
        }
        return textureHandle[0];
    }

    /**
     * 创建纹理
     * @param textureTarget Texture类型。
     *                      1. 相机用 GLES11Ext.GL_TEXTURE_EXTERNAL_OES
     *                      2. 图片用 GLES20.GL_TEXTURE_2D
     * @param count         创建纹理数量
     * @param minFilter     缩小过滤类型 (1.GL_NEAREST ; 2.GL_LINEAR)
     * @param magFilter     放大过滤类型
     * @param wrapS         纹理S方向边缘环绕;也称作X方向
     * @param wrapT         纹理T方向边缘环绕;也称作Y方向
     * @return 返回创建的 Texture ID
     */
    public static int[] createTextures(int textureTarget, int count, int minFilter, int magFilter, int wrapS,
                                       int wrapT) {
        int[] textureHandles = new int[count];
        for (int i = 0; i < count; i++) {
            // 1.生成纹理
            GLES20.glGenTextures(1, textureHandles, i);
            // 2.绑定纹理
            GLES20.glBindTexture(textureTarget, textureHandles[i]);
            // 3.设置纹理属性
            // 设置纹理的缩小过滤类型(1.GL_NEAREST ; 2.GL_LINEAR)
            GLES20.glTexParameterf(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, minFilter);
            // 设置纹理的放大过滤类型(1.GL_NEAREST ; 2.GL_LINEAR)
            GLES20.glTexParameterf(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, magFilter);
            // 设置纹理的X方向边缘环绕
            GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, wrapS);
            // 设置纹理的Y方向边缘环绕
            GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, wrapT);
        }
        return textureHandles;
    }
}

二、绘制纹理

  下面开始正式介绍如何将Bitmap绘制到四边形上。首先从GLSL的顶点着色器和片段着色器的代码开始。

2.1 纹理GLSL

  与Android OpenGL基础(一、绘制三角形四边形)中的四边形不同的是,本次需要将纹理绘制到四边形上,所以对顶点着色器和片段着色器的GLSL代码改动如下:

class BitmapSquare {
    /**
     * 顶点着色器代码;
     */
    private val vertexShaderCode =
                // 等待输入的纹理顶点坐标
                "attribute vec4 inputTextureCoordinate;" +
                " varying vec2 textureCoordinate;" +
                "attribute vec4 vPosition;" +
                "void main() {" +
                "  gl_Position = vPosition;" +
                "textureCoordinate = inputTextureCoordinate.xy;" +
                "}"

    /**
     * 片段着色器代码
     */
    private val fragmentShaderCode =
        "varying highp vec2 textureCoordinate;" +
                "uniform sampler2D inputImageTexture;" +
                "void main() {" +
                // 将2D纹理inputImageTexture和纹理顶点坐标通过texture2D计算后传给片段着色器
                "  gl_FragColor = texture2D(inputImageTexture, textureCoordinate);" +
                "}"
}

  重点关注的地方是:(1)新增了inputTextureCoordinate用于程序中传入纹理顶点坐标;(2)使用了GLSL内建的texture2D函数来采样纹理的颜色,传给片段着色器。

2.2 创建纹理

  在1.2小节已经介绍了创建纹理的工具类,下面直接使用。MyGLSurfaceView代码与之前大体相同,只新增了创建Bitmap,然后将Bitmap传递给Render:

class MyGLSurfaceView(context: Context?, attrs: AttributeSet?) : GLSurfaceView(context, attrs) {
    private val renderer: MyGLRenderer

    init {
        setEGLContextClientVersion(2)
        renderer = MyGLRenderer()
        setRenderer(renderer)
        renderMode = RENDERMODE_WHEN_DIRTY
        val bitmap = BitmapFactory.decodeResource(context?.resources, R.drawable.adventure_time)
        renderer.setImageBitmap(bitmap)
    }
}

  创建纹理的时机与Android OpenGL基础(一、绘制三角形四边形)类似,都在onSurfaceCreated方法中,创建纹理后用glTextureId持有该纹理:

class MyGLRenderer : GLSurfaceView.Renderer {

    private lateinit var bitmapSquare: BitmapSquare
    // 纹理ID
    private var glTextureId = 0
    private var bitmap: Bitmap? = null

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
        bitmapSquare = BitmapSquare()
        // 创建纹理
        glTextureId = OpenGLUtils.createTexture(bitmap, GLES20.GL_NEAREST, GLES20.GL_LINEAR, 
            GLES20.GL_CLAMP_TO_EDGE, GLES20.GL_CLAMP_TO_EDGE)
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        bitmapSquare.draw(glTextureId)
    }

    fun setImageBitmap(bitmap: Bitmap) {
        this.bitmap = bitmap
    }
}

2.3 开始绘制

  在创建完纹理后,需要做的就是在执行onDrawFrame方法时,激活纹理并将纹理顶点传递给OpenGL。

2.3.1 纹理顶点

  与顶点坐标类似,纹理顶点也采用了FloatArray,在传递给OpenGL时使用FloatBuffer。纹理所对应的顶点使用的是纹理坐标系。

    // 顶点所对应的纹理坐标
    private var texVertex = floatArrayOf(
        0f, 0f,      // top left
        0f, 1f,      // bottom left
        1f, 1f,       // bottom right
        1f, 0f     // top right
    )
    // 四个纹理顶点的缓冲数组
    private val texVertexBuffer: FloatBuffer =
        ByteBuffer.allocateDirect(texVertex.size * 4).order(ByteOrder.nativeOrder())
            .asFloatBuffer().apply {
                put(texVertex)
                position(0)
            }

2.3.2 onDrawFrame绘制

  在执行onDrawFrame绘制的过程中,设置纹理顶点即可绘制出纹理,设置纹理顶点坐标的方法也与设置四边形顶点类似:

// 激活textureID对应的纹理单元
GLES20.glActiveTexture(textureID)
// 绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureID)
// 获取顶点着色器中的inputTextureCoordinate变量(纹理坐标);用唯一ID表示
val textureCoordinate = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate")
// 允许操作纹理坐标inputTextureCoordinate变量
GLES20.glEnableVertexAttribArray(textureCoordinate)
// 将纹理坐标数据传递给inputTextureCoordinate变量
GLES20.glVertexAttribPointer(
    textureCoordinate, COORDS_PER_TEXTURE_VERTEX, GLES20.GL_FLOAT,
    false, textVertexStride, texVertexBuffer
)

  完整代码如下:

class BitmapSquare {
    // 每个顶点的坐标数
    private val COORDS_PER_VERTEX = 3
    // 每个纹理顶点的坐标数
    private val COORDS_PER_TEXTURE_VERTEX = 2
    // 顶点的坐标
    private var squareCoords = floatArrayOf(
        -0.5f, 0.5f, 0.0f,      // top left
        -0.5f, -0.5f, 0.0f,      // bottom left
        0.5f, -0.5f, 0.0f,      // bottom right
        0.5f, 0.5f, 0.0f       // top right
    )
    // 顶点所对应的纹理坐标
    private var texVertex = floatArrayOf(
        0f, 0f,      // top left
        0f, 1f,      // bottom left
        1f, 1f,       // bottom right
        1f, 0f     // top right
    )
    // 四个顶点的缓冲数组
    private val vertexBuffer: FloatBuffer =
        ByteBuffer.allocateDirect(squareCoords.size * 4).order(ByteOrder.nativeOrder())
            .asFloatBuffer().apply {
                put(squareCoords)
                position(0)
            }
    // 四个顶点的绘制顺序数组
    private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3)

    // 四个顶点绘制顺序数组的缓冲数组
    private val drawListBuffer: ShortBuffer =
        ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder())
            .asShortBuffer().apply {
                put(drawOrder)
                position(0)
            }

    // 四个纹理顶点的缓冲数组
    private val texVertexBuffer: FloatBuffer =
        ByteBuffer.allocateDirect(texVertex.size * 4).order(ByteOrder.nativeOrder())
            .asFloatBuffer().apply {
                put(texVertex)
                position(0)
            }

    private var vPMatrixHandle: Int = 0

    /**
     * 顶点着色器代码;
     */
    private val vertexShaderCode =
                "attribute vec4 inputTextureCoordinate;" +
                " varying vec2 textureCoordinate;" +
                "attribute vec4 vPosition;" +
                "void main() {" +
                // 把vPosition顶点经过矩阵变换后传给gl_Position
                "  gl_Position = vPosition;" +
                "textureCoordinate = inputTextureCoordinate.xy;" +
                "}"

    /**
     * 片段着色器代码
     */
    private val fragmentShaderCode =
        "varying highp vec2 textureCoordinate;" +
                "uniform sampler2D inputImageTexture;" +
                "void main() {" +
                "  gl_FragColor = texture2D(inputImageTexture, textureCoordinate);" +
                "}"
    /**
     * 着色器程序ID引用
     */
    private var mProgram: Int

    init {
        // 编译顶点着色器和片段着色器
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        // glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
        mProgram = GLES20.glCreateProgram().also {
            // 把顶点着色器添加到程序对象
            GLES20.glAttachShader(it, vertexShader)
            // 把片段着色器添加到程序对象
            GLES20.glAttachShader(it, fragmentShader)
            // 连接并创建一个可执行的OpenGL ES程序对象
            GLES20.glLinkProgram(it)
        }
    }

    private fun loadShader(type: Int, shaderCode: String): Int {
        // glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
        val shader = GLES20.glCreateShader(type)
        // 把着色器和代码关联,然后编译着色器
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
        return shader
    }

    private val vertexStride: Int = COORDS_PER_VERTEX * 4
    private val textVertexStride: Int = COORDS_PER_TEXTURE_VERTEX * 4

    fun draw(textureID: Int) {
        // 激活着色器程序 Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram)
        // 获取顶点着色器中的vPosition变量(因为之前已经编译过着色器代码,所以可以从着色器程序中获取);用唯一ID表示
        val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
        // 允许操作顶点对象position
        GLES20.glEnableVertexAttribArray(position)
        // 将顶点数据传递给position指向的vPosition变量;将顶点属性与顶点缓冲对象关联
        GLES20.glVertexAttribPointer(
            position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
            false, vertexStride, vertexBuffer
        )
        // 激活textureID对应的纹理单元
        GLES20.glActiveTexture(textureID)
        // 绑定纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureID)
        // 获取顶点着色器中的inputTextureCoordinate变量(纹理坐标);用唯一ID表示
        val textureCoordinate = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate")
        // 允许操作纹理坐标inputTextureCoordinate变量
        GLES20.glEnableVertexAttribArray(textureCoordinate)
        // 将纹理坐标数据传递给inputTextureCoordinate变量
        GLES20.glVertexAttribPointer(
            textureCoordinate, COORDS_PER_TEXTURE_VERTEX, GLES20.GL_FLOAT,
            false, textVertexStride, texVertexBuffer
        )
        // 按drawListBuffer中指定的顺序绘制四边形
        GLES20.glDrawElements(
            GLES20.GL_TRIANGLE_STRIP, drawOrder.size,
            GLES20.GL_UNSIGNED_SHORT, drawListBuffer
        )
        // 操作完后,取消允许操作顶点对象position
        GLES20.glDisableVertexAttribArray(position)
        GLES20.glDisableVertexAttribArray(textureCoordinate)
    }
}

  在程序运行后,就可以看到如下效果:

image.png   右边是把四边形顶点坐标设置为(1,1)效果。

    private var rightSquareCoords = floatArrayOf(
        -1f, 1f, 0.0f,      // top left
        -1f, -1f, 0.0f,      // bottom left
        1f, -1f, 0.0f,      // bottom right
        1f, 1f, 0.0f       // top right
    )

2.4 纹理单元

  上面代码中使用GLES20.glActiveTexture(textureID)来激活textureID所对应的纹理单元。OpenGLES20至少保证有32个纹理单元供你使用,也就是说你可以激活从GLES20.GL_TEXTURE0到GLES20.GL_TEXTRUE31。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8,这在当我们需要循环一些纹理单元的时候会很有用。

三、纹理过滤

  下面来介绍创建纹理时,常用的几个属性,首先介绍纹理过滤(minFilter和magFilter)。

public class OpenGLUtils {
    /**
     * 创建纹理
     * @param textureTarget Texture类型。
     *                      1. 相机用 GLES11Ext.GL_TEXTURE_EXTERNAL_OES
     *                      2. 图片用 GLES20.GL_TEXTURE_2D
     * @param count         创建纹理数量
     * @param minFilter     缩小过滤类型 (1.GL_NEAREST ; 2.GL_LINEAR)
     * @param magFilter     放大过滤类型
     * @param wrapS         纹理S方向边缘环绕;也称作X方向
     * @param wrapT         纹理T方向边缘环绕;也称作Y方向
     * @return 返回创建的 Texture ID
     */
    public static int[] createTextures(int textureTarget, int count, int minFilter,
    int magFilter, int wrapS, int wrapT) {
    }
}

  纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,当纹理与实际物体的分辨率不同时,OpenGL需要知道怎样将纹理像素(Texture Pixel,也叫Texel)映射到纹理坐标。OpenGL对于纹理过滤(Texture Filtering)有很多个选项,常用的有两种:GL_NEARESTGL_LINEAR

3.1 邻近过滤

  GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:

3.2 线性过滤

  GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:

3.3 对比

  GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR可以产生更真实的输出,但有些开发者更喜欢8-bit风格,所以他们会用GL_NEAREST选项。
  当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。

四、纹理环绕方式

  下面介绍纹理环绕方式(wrapS和wrapT)。

public class OpenGLUtils {
    /**
     * 创建纹理
     * @param textureTarget Texture类型。
     *                      1. 相机用 GLES11Ext.GL_TEXTURE_EXTERNAL_OES
     *                      2. 图片用 GLES20.GL_TEXTURE_2D
     * @param count         创建纹理数量
     * @param minFilter     缩小过滤类型 (1.GL_NEAREST ; 2.GL_LINEAR)
     * @param magFilter     放大过滤类型
     * @param wrapS         纹理S方向边缘环绕;也称作X方向
     * @param wrapT         纹理T方向边缘环绕;也称作Y方向
     * @return 返回创建的 Texture ID
     */
    public static int[] createTextures(int textureTarget, int count, int minFilter,
    int magFilter, int wrapS, int wrapT) {
    }
}

  纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像,此外OpenGL提供了更多的选择:

环绕方式描述
GL_REPEAT对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEATGL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER超出的坐标为用户指定的边缘颜色。

  当纹理坐标超出默认范围时,不同的纹理环绕方式效果如下:

The End

欢迎关注我,一起解锁更多技能:BC的主页~~💐💐💐 个人信息汇总.png

Android OpenGL开发者文档:developer.android.com/guide/topic…
opengl学习资料:learnopengl-cn.github.io/
Android OpenGL基础(一、绘制三角形四边形):juejin.cn/post/707675…
Android OpenGL基础(二、坐标系统):juejin.cn/post/707713…
Android OpenGL基础(三、绘制Bitmap纹理):juejin.cn/post/707967…
Android OpenGL基础专栏:juejin.cn/column/7076…