4b579a0ab1203115e2d57e930505ee65
Learn OpenGL 4. 纹理

预备知识

纹理

纹理是一个用来保存图像的颜色元素值的 OpenGL ES 缓存。在这里,我们可以简单理解它是图片/贴图。

2D 纹理坐标

纹理坐标在 X, Y 轴上,范围从 0 到 1。纹理坐标左下角为 (0, 0),右上角为 (1, 1)。这点和我们之前使用的 OpenGL 坐标不同。

<img src="https://images.xiaozhuanlan.com/photo/2019/6b1ccd61c1fce546a3185d5cba37c148.png" width= "400" alt="坐标系" align=center/>

动手画画

修改顶点

定义顶点属性

由于在本节中,我们需要知道纹理坐标才能进行渲染(顶点对应的纹理坐标),所以需要增加纹理坐标属性。

typealias Position = (x: GLfloat, y: GLfloat, z: GLfloat)
typealias Color = (r: GLfloat, g: GLfloat, b: GLfloat, a: GLfloat)
typealias TextureCoordinate = (u: GLfloat, v: GLfloat)

struct L4Vertex {

    // 顶点坐标
    let position: Position
    // 顶点颜色
    var color: Color
    // 纹理坐标
    var texCoord: TextureCoordinate
}

定义顶点数组

// 顶点数组
private let vertices: [L4Vertex] = [
    L4Vertex(position: (-1, -1, 0), color: (1, 0, 0, 1), texCoord: (0, 0)),
    L4Vertex(position: (1, -1, 0), color: (0, 1, 0, 1), texCoord: (1, 0)),
    L4Vertex(position: (1, 1, 0), color: (0, 0, 1, 1), texCoord: (1, 1)),

    L4Vertex(position: (1, 1, 0), color: (0, 0, 1, 1), texCoord: (1, 1)),
    L4Vertex(position: (-1, 1, 0), color: (0, 1, 0, 1), texCoord: (0, 1)),
    L4Vertex(position: (-1, -1, 0), color: (1, 0, 0, 1), texCoord: (0, 0))
]

例如,顶点数组的第一个顶点就是将 OpenGL 左下角的坐标对应纹理坐标的左下角坐标。

定义 Shader

由于入参(多了纹理顶点属性以及纹理信息)改变,所以在 Shader 中也要增加相应的代码。

顶点着色器

可以在下面的代码看到,我们在顶点着色器中接收纹理坐标属性,并将它传输到片段着色器中。

// l4vertex.vsh
attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord;

varying lowp vec4 frag_color;
varying lowp vec2 frag_texCoord;

void main(void) {
  frag_color = a_color;
  frag_texCoord = a_texCoord;
  gl_Position = a_position;
}

4 行是我们新增的纹理坐标,因为是平面坐标,所以声明为 vec2。
7 行是我们需要向片段着色器传递的纹理坐标。

片段着色器

GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler)。我们待会可以通过指定不同采样器来渲染不同的纹理。

// l4fragment.fsh
varying lowp vec4 frag_color;
varying lowp vec2 frag_texCoord;

uniform sampler2D u_texture;

void main(void) {
  gl_FragColor = texture2D(u_texture, frag_texCoord);
}

5 行中的 Sampler2D 是 GLSL 的内置数据类型。
8 行中的 texture2D 是 GLSL 内置函数,它的第一个参数是选择对应的采样器,第二个参数是对应的纹理坐标 。

修改 Base Effect

这里继续沿用上一节中我们自定义的 Custom Base Effect。不同的代码在于链接阶段,我们需要绑定新的纹理坐标顶点属性,还有需要新增参数来获取 u_texture 这个 uniform 变量的索引值以便传参

private func compile(_ vertexShaderName: String, _ fragmentShaderName: String) {
    ...

    // 绑定顶点着色器的顶点属性
    glBindAttribLocation(program, VertexAttrib.position.rawValue, "a_position")
    glBindAttribLocation(program, VertexAttrib.color.rawValue, "a_color")
    glBindAttribLocation(program, VertexAttrib.textureCoordinate.rawValue, "a_texCoord")

    // 链接程序
    glLinkProgram(program)

    // 查询 uniform 信息
    textureUniform = glGetUniformLocation(program, "u_texture")

    var success = GLint()
    ...
}

glGetUniformLocation (GLuint program, const GLchar* name) 的作用是查询程序中 Uniform 变量的索引位置,然后通过索引位置向该参数传值。(如果忘了变量类型可以在上一节中找到。)

加载纹理

这里我们通过 GLKit 提供的函数来获取纹理信息。这里我们传入的参数的我们想要展示的图片名称,options 中传入 [GLKTextureLoaderOriginBottomLeft: true] 是为了解决图片上下颠倒的问题(因为纹理坐标和 UIKit坐标刚好相反,GLKit 通过传入对应 options 来帮我们解决这个问题)。

private func loadTexture(_ resource: String) -> GLuint? {
    guard
        let path = Bundle.main.path(forResource: resource, ofType: nil),
        let info = try? GLKTextureLoader.texture(
            withContentsOfFile: path,
            options: [GLKTextureLoaderOriginBottomLeft: true]
        ) else
    {
        print("[Load Texture Error]")
        return nil
    }

    return info.name
}

然后我们在 Base Effect 中暴露一个接口给外部来传入图片名称,texture 变量的作用是用来保存纹理信息的索引值,以便我们在渲染过程中传入纹理索引到 OpenGL 状态机中。

private var texture: GLuint?

func setTexture(_ resource: String) {
    texture = loadTexture(resource)
}

渲染过程中,我们需要告诉着色器目前需要用哪个采样器(就是用哪个纹理)。

func prepareToDraw() {
    glUseProgram(program)

    if let texture = texture {
        // 其实 GL_TEXTURE0 是默认开启的
        glActiveTexture(GLenum(GL_TEXTURE0))
        // 绑定纹理
        glBindTexture(GLenum(GL_TEXTURE_2D), texture)
        // 给片段着色器采样器变量 Sample2D 赋值
        // 其实就是告诉采样器从哪个 TEXTURE 中读取信息
        glUniform1i(textureUniform, 0)
    }
}

有个小插曲,其实在这里 prepareToDraw 函数一行都不用修改都可以达到渲染目的。
因为 GL_TEXTURE0 是默认开启的,所以 6 行在这里可以省略。同时 GLKit 的 texture(withContentsOfFile path: String, options: [String : NSNumber]? = nil) throws 加载纹理函数会将纹理绑定到对应的纹理索引,也就是 8 行也可以省略。至于 11 行,不赋值 textureUniform 会默认为 0。
但是为了上下文完整性,我们还是加上了(有兴趣的同学可以自己实验一下。)

纹理绑定三部曲

1. 激活纹理单元<br>
glActiveTexture (GLenum texture)

texture: 需要启用的 texture 的索引,GL_TEXTURE0 是默认开启
2. 绑定纹理<br>
glBindTexture (GLenum target, GLuint texture)

target: 由于我们渲染是 2D 纹理,所以传入 target 是 GL_TEXTURE_2D
texture: 这里是传入我们上面纹理的索引值
3. 告诉 OpenGL 当前使用哪个采样器<br>
glUniform1i (GLint location, GLint x)

location: 片段着色器中接收采样器的索引
x: 启用哪个纹理索引,和 glActiveTexture 中的索引对应

传输顶点数据

这里和前几节中的传输顶点数据是类似的,只是增加了对 textureCoordinate 属性的传输。

private func setupVertexAttributes() {

    ...

    glEnableVertexAttribArray(VertexAttrib.textureCoordinate.rawValue)

    // 传输纹理坐标
    glVertexAttribPointer(
        VertexAttrib.textureCoordinate.rawValue,
        2,
        GLenum(GL_FLOAT),
        GLboolean(GL_FALSE),
        GLsizei(MemoryLayout<L4Vertex>.stride),
        UnsafeRawPointer(bitPattern: (3 + 4) * MemoryLayout<GLfloat>.size)
    )
}

渲染

我们在初始化 BaseEffect 的地方,加上一行调用 setTexture(_ resource: String) 的代码即可。原本 glkView(_ view: GLKView, drawIn rect: CGRect) 中的代码都不需要改动。

// 注意: effect 的初始化需要在设置 EAGLContext.setCurrent 后
private lazy var effect: L4BaseEffect = {
    let effect = L4BaseEffect("l4vertex.vsh", "l4fragment.fsh")
    effect.setTexture("L4.JPG")
    return effect
}()

效果

如果我们的代码一切都正常,那我们就会看到下面这样的效果。
上面的代码可以在 https://github.com/xurunkang/learnopengl 中找到。

实验

可以尝试将纹理与原本的顶点颜色进行组合,生成滤镜效果。(提示:只要在片段着色器中将纹理与原本的顶点颜色相乘即可,答案在小结部分)

小结

  • 了解到了如何加载纹理以及传递 uniform 修饰参数上下文。

实验答案

只需要把片段着色器的 gl_FragColor 的赋值改为下面一行即可。

void main(void) {
  gl_FragColor = texture2D(u_texture, frag_texCoord) * frag_color;
}
© 著作权归作者所有
这个作品真棒,我要支持一下!
🧀 专栏介绍 - 记录日常开发和学习中遇到的知识/问题 🚀 专栏方向 目前专注于 - OpenGL - ...
0条评论
top Created with Sketch.