LearnOpenGL - Android OpenGL ES 3.0 绘制纹理

系列文章目录



一、前言

LearnOpenGL - Android OpenGL ES 3.0 绘制三角形 中我们学会了如何在 Android 下搭建 GLES 环境,并绘制三角形。本文我们将讨论如何绘制纹理。
本文代码在 TextureDrawer

二、数据流:顶点着色器到片元着色器

在进入具体的代码细节先,我想说明顶点着色器与片元着色器是如何工作的,它们之间是如何联系在一起的,它们的输入和输出分别是什么。

2.1 顶点着色器

顶点着色器的输入是顶点属性。这些属性通常包括顶点的位置、颜色、法线、纹理坐标等。顶点着色器处理这些输入并生成顶点的输出,这些输出通常会被传递给片元着色器。、

如果有 4 个顶点,那么顶点着色器会被执行 4 次。每次执行时,顶点着色器都会处理一个顶点的属性,并生成对应的输出。下面是一个更详细的解释:

直观解释

  1. 顶点数据: 你定义了一组顶点数据。例如,假设你有一个包含 4 个顶点的简单模型,每个顶点都有位置和颜色属性。

  2. 顶点着色器执行:

    • 当你开始渲染这个模型时,渲染管线会将顶点数据传递给顶点着色器。
    • 顶点着色器会为每个顶点单独执行一次。因此,如果有 4 个顶点,顶点着色器会执行 4 次。
    • 每次执行时,顶点着色器都会读取一个顶点的属性数据(例如位置和颜色),进行必要的处理(例如变换位置,传递颜色),并生成对应的输出。

示例

假设你有以下顶点数据(每个顶点包含位置和颜色):

GLfloat vertices[] = {
    // 位置        // 颜色
    0.0f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f, 1.0f, // 顶点 1: 红色
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f, 1.0f, // 顶点 2: 绿色
    0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f, 1.0f, // 顶点 3: 蓝色
    0.5f,  0.5f, 0.0f,  1.0f, 1.0f, 0.0f, 1.0f  // 顶点 4: 黄色
};

对于这些顶点数据,顶点着色器类似于以下内容:

#version 300 es
layout(location = 0) in vec3 aPosition; // 顶点位置
layout(location = 1) in vec4 aColor;    // 顶点颜色

out vec4 vColor; // 传递给片元着色器的输出变量

void main()
{
    gl_Position = vec4(aPosition, 1.0); // 将顶点位置转换为齐次坐标
    vColor = aColor; // 将顶点颜色传递给片元着色器
}

执行流程

  • 第一次执行: 处理第一个顶点,输入 aPosition = vec3(0.0, 0.5, 0.0)aColor = vec4(1.0, 0.0, 0.0, 1.0)
  • 第二次执行: 处理第二个顶点,输入 aPosition = vec3(-0.5, -0.5, 0.0)aColor = vec4(0.0, 1.0, 0.0, 1.0)
  • 第三次执行: 处理第三个顶点,输入 aPosition = vec3(0.5, -0.5, 0.0)aColor = vec4(0.0, 0.0, 1.0, 1.0)
  • 第四次执行: 处理第四个顶点,输入 aPosition = vec3(0.5, 0.5, 0.0)aColor = vec4(1.0, 1.0, 0.0, 1.0)

每次执行时,顶点着色器根据输入的顶点属性计算输出的 gl_PositionvColor。这些输出随后传递给后续的渲染管线阶段,例如图元装配、光栅化和片元着色器。

2.2 片元着色器

片元着色器的执行次数取决于最终渲染到屏幕上的像素数量,而不是输入的顶点数量。片元着色器会为每个生成的片元(像素)执行一次。具体执行次数取决于渲染的几何图形覆盖的屏幕区域。以下是详细的解释:

片元着色器执行次数

  1. 图元的类型:

    • 图元类型可以是点、线、三角形等。
    • 通常情况下,渲染的是三角形。
  2. 图元覆盖的屏幕区域:

    • 图元被光栅化(rasterized)成片元。
    • 每个片元对应屏幕上的一个像素。
  3. 片元着色器执行:

    • 片元着色器会为每个片元(像素)执行一次。
    • 如果一个三角形覆盖了 100 个像素,片元着色器就会执行 100 次。

示例

假设我们有一个包含 4 个顶点的四边形,由两个三角形组成,渲染到屏幕上覆盖一定区域。

GLfloat vertices[] = {
    // 位置         // 颜色
    -0.5f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f, 1.0f, // 顶点 1: 红色
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f, 1.0f, // 顶点 2: 绿色
     0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f, 1.0f, // 顶点 3: 蓝色
     0.5f,  0.5f, 0.0f,  1.0f, 1.0f, 0.0f, 1.0f  // 顶点 4: 黄色
};

GLuint indices[] = {
    0, 1, 2, // 第一个三角形
    0, 2, 3  // 第二个三角形
};

片元着色器代码示例

#version 300 es
precision mediump float;

in vec4 vColor; // 从顶点着色器传递过来的颜色
out vec4 FragColor; // 输出的片元颜色

void main()
{
    FragColor = vColor; // 将颜色输出
}

渲染流程

  1. 顶点着色器阶段:

    • 对每个顶点执行一次,共执行 4 次。
    • 顶点着色器输出顶点的齐次坐标和颜色。
  2. 图元装配和光栅化阶段:

    • 将顶点装配成两个三角形。
    • 将两个三角形转换为片元(像素)。
  3. 片元着色器阶段:

    • 每个片元调用一次片元着色器。
    • 如果两个三角形覆盖了 200 个像素,片元着色器会执行 200 次。

总结

  • 顶点着色器: 执行次数与顶点数量相同,每个顶点执行一次。
  • 片元着色器: 执行次数与片元(像素)数量相同,每个片元执行一次。

因此,片元着色器的执行次数通常远多于顶点着色器的执行次数,因为片元数量通常大于顶点数量。具体执行次数取决于渲染的几何图形在屏幕上的覆盖范围。

2.3 顶点着色器到片元着色器

用 WebGL编程指南 中一张图片来总结上面提到的内容
在这里插入图片描述
在这里插入图片描述

三、纹理绘制流程

先从着色器出发,思考如何编写着色器,然后根据着色器来编写 OpenGL ES 代码

3.1 顶点着色器

输入:为了绘制一张矩形的图片,我们需要 4 个顶点,每个顶点的属性包括顶点位置和纹理坐标。因此,需要定义两个输入。
输出:将纹理坐标传递给片元着色器,以便在片元着色器中进行纹理采样和其他操作。因此需要定义一个输出。

#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texcoord;
out vec2 v_texcoord;
void main()
{
    gl_Position = a_position;
    v_texcoord = a_texcoord;
}

3.2 片元着色器

输入:绘制纹理,那么自然需要一张纹理;此外,还有纹理坐标(已经被插值过了)
输出:片元的颜色

#version 300 es
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
    fragColor = texture(texture0, v_texcoord);
}

3.3 验证着色器

在编写 OpenGL ES 代码前,让我们先验证下 shader 是否正确,这里推荐使用 KodeLife,它支持顶点着色器和片元着色器,网上很多在线的 shader 网站只支持片元着色器。

本人在 Mac 运行 KodeLife ,想要运行上面代码需要做一些修改,你需要将 version 信息修改为 “#version 330”,接着在 KodeLife 上导入一张纹理即可。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到我们的 shader 能够正常的渲染出图片,只是图片颠倒了,但这个问题可以在 Android 加载图片的时候进行处理。

3.4 代码编写

完整代码在 TextureDrawer

代码分为两个部分:prepare函数和draw函数。

prepare函数

prepare函数用于初始化和准备所有需要的OpenGL资源,包括着色器、VAO、VBO、EBO以及纹理。

1. 编译着色器
sharer.prepareShaders()
checkGlError("compile shader")

调用sharer.prepareShaders()编译着色器,并使用checkGlError检查是否有任何OpenGL错误。

2. 准备顶点缓冲区
val vertexBuffer = ByteBuffer
    .allocateDirect(vertices.size * Float.SIZE_BYTES)
    .order(ByteOrder.nativeOrder())
    .asFloatBuffer()
    .apply {
        put(vertices)
        position(0)
    }

创建一个直接字节缓冲区,并将顶点数据放入缓冲区中。

3. 生成VAO、VBO和EBO
GLES30.glGenVertexArrays(1, vaos)
GLES30.glGenBuffers(1, vbos)
GLES30.glGenBuffers(1, ebo)
checkGlError("gen vertex array and buffer")

生成一个VAO,一个VBO和一个EBO,并检查是否有任何OpenGL错误。

4. 绑定并设置VAO
GLES30.glBindVertexArray(vaos[0])

绑定生成的VAO。

5. 设置VBO数据
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbos[0])
GLES30.glBufferData(
    GLES30.GL_ARRAY_BUFFER,
    Float.SIZE_BYTES * vertices.size,
    vertexBuffer,
    GLES30.GL_STATIC_DRAW
)
checkGlError("glBufferData")

绑定VBO并将顶点数据传递给缓冲区。

6. 设置EBO数据
val indexBuffer = ByteBuffer
    .allocateDirect(indices.size * Int.SIZE_BYTES)
    .order(ByteOrder.nativeOrder())
    .asIntBuffer()
    .apply {
        put(indices)
        position(0)
    }
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, ebo[0])
GLES30.glBufferData(
    GLES30.GL_ELEMENT_ARRAY_BUFFER,
    Int.SIZE_BYTES * indices.size,
    indexBuffer,
    GLES30.GL_STATIC_DRAW
)
checkGlError("glBufferData for indices")

创建索引缓冲区并将索引数据传递给EBO。

7. 设置VAO属性
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 5 * Float.SIZE_BYTES, 0)
GLES30.glEnableVertexAttribArray(0)
GLES30.glVertexAttribPointer(
    1,
    2,
    GLES30.GL_FLOAT,
    false,
    5 * Float.SIZE_BYTES,
    3 * Float.SIZE_BYTES
)
GLES30.glEnableVertexAttribArray(1)

设置顶点属性指针和启用顶点属性。这里有两个属性:位置(3个float)和纹理坐标(2个float)。

8. 解绑VAO
GLES30.glBindVertexArray(0)

解绑VAO。

9. 准备纹理
GLES30.glGenTextures(texIds.capacity(), texIds)
checkGlError("glGenTextures")
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])
checkGlError("glBindTexture")

生成纹理ID并绑定纹理。

10. 设置纹理过滤
GLES30.glTexParameteri(
    GLES30.GL_TEXTURE_2D,
    GLES30.GL_TEXTURE_MIN_FILTER,
    GLES30.GL_NEAREST
)
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR)
checkGlError("glTexParameteri")

设置纹理过滤参数。

11. 设置纹理图像数据
val options = BitmapFactory.Options()
options.inScaled = false
var bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.lye, options)
val matrix = android.graphics.Matrix()
matrix.preScale(1.0f, -1.0f)
bitmap = android.graphics.Bitmap.createBitmap(
    bitmap,
    0,
    0,
    bitmap.width,
    bitmap.height,
    matrix,
    false
)
GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0)
checkGlError("texImage2D")
bitmap.recycle()

解码资源中的图片,并垂直翻转,然后将图像数据传递给纹理。

12. 解绑纹理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)

解绑纹理。

13. 使用着色器程序并设置纹理位置
sharer.use()
sharer.setInt("texture1", 0)

使用着色器程序,并设置纹理单元。

draw函数

draw函数用于实际绘制帧。

1. 清除颜色缓冲区
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)

清除颜色缓冲区。

2. 绑定VAO
GLES30.glBindVertexArray(vaos[0])

绑定VAO。

3. 激活并绑定纹理
GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])

激活纹理单元并绑定纹理。

4. 绘制元素
GLES30.glDrawElements(GLES30.GL_TRIANGLES, indices.size, GLES30.GL_UNSIGNED_INT, 0)

绘制元素。

5. 解绑VAO
GLES30.glBindVertexArray(0)

解绑VAO。

四、其他问题

4.1 为什么图片填充至纹理后是颠倒的

我是这么想的,图片填充时使用的内存起始地址是图片的左上角,然后一行一行地从上到下将数据拷贝到 GPU 纹理上,而纹理的起始地址是右下角,因为这种差异导致了颠倒。
在这里插入图片描述
因此为了让图片正确显示,我们可以

  1. 在图像加载阶段进行调整:在将图像数据传递给OpenGL之前,将图像上下翻转。这种方法在代码中已经实现,通过Matrix.preScale(1.0f, -1.0f)完成。
// Flip the bitmap vertically
val matrix = android.graphics.Matrix()
matrix.preScale(1.0f, -1.0f)
bitmap = android.graphics.Bitmap.createBitmap(
    bitmap,
    0,
    0,
    bitmap.width,
    bitmap.height,
    matrix,
    false
)
  1. 在着色器阶段进行调整:在顶点着色器或片段着色器中,翻转纹理坐标。
#version 300 es

layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texcoord;

out vec2 v_texcoord;

void main()
{
    gl_Position = a_position;
    v_texcoord = vec2(a_texcoord.x, 1.0 - a_texcoord.y); // Flip y axis
}

参考

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android中使用OpenGL ES 3.0加载纹理有以下步骤: 1. 初始化OpenGL ES环境:在OpenGL ES操作前,需要初始化OpenGL ES环境,创建GLSurfaceView实例,并设置渲染器。 2. 创建纹理对象:使用OpenGL ES函数glGenTextures生成纹理对象ID,并绑定到当前活动的纹理单元。 3. 加载纹理图像:通过Android的Bitmap对象加载纹理图像,可以使用BitmapFactory类的decodeResource方法从资源中加载或使用Bitmap类的createBitmap方法动态创建。调用glTexImage2D将图像数据传输到纹理对象中。 4. 设置纹理过滤器:可以使用glTexParameteri函数设置纹理的放大缩小过滤器,如GL_LINEAR或GL_NEAREST。 5. 设置纹理包裹模式:可以使用glTexParameteri函数设置纹理的包裹模式,如GL_CLAMP_TO_EDGE或GL_REPEAT。 6. 加载纹理坐标:通过定义纹理坐标数组,确定纹理映射到对象的哪个部分。在顶点着色器中使用纹理坐标进行纹理采样。 7. 绘制纹理:在绘制OpenGL场景时,通过在顶点着色器和片段着色器中使用纹理坐标,并利用纹理采样器将纹理应用到对象上。 8. 释放纹理资源:当纹理不再使用时,需要调用glDeleteTextures函数释放纹理对象。 总之,使用OpenGL ES 3.0加载纹理需要创建纹理对象、加载纹理图像、设置纹理过滤器与包裹模式、加载纹理坐标,并在绘制时通过纹理采样器将纹理应用到对象上。记得释放纹理资源,以避免内存泄漏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值