E91962b78b33a022f7d5a9e3ceee4402
Learn OpenGL 3. 着色器

预备知识

在前几节中,我们都是通过 GLKit 的 GLKBaseEffect 来实现我们的效果。在这节中,我们将使用自定义的 Shader 来实现 Custom Base Effect 从而更加深入地了解 OpenGL。

Shader

Shaders are little programs that rest on the GPU. These programs are run for each specific section of the graphics pipeline. In a basic sense, shaders are nothing more than programs transforming inputs to outputs. Shaders are also very isolated programs in that they're not allowed to communicate with each other; the only communication they have is via their inputs and outputs.

着色器实际上是是运行在 GPU 上的微程序。这些微程序是为了渲染管线中的每个特定的部分而运行。从基本的意义上讲,着色器只不过是将输入转换为输出的程序。着色器也是非常独立的程序,因为它们彼此间不允许通信;他们只能通过他们的输入和输出来传递上下文。

而着色器又分为 Vertex Shader(顶点着色器) 和 Fragment Shader(片段着色器)。

OpenGL Shading Language (GLSL)

着色器是由 OpenGL Shading Language 的类 C 语言编写的。

我们目前只需要了解以下的信息:

向量

变量类型

uniform

uniform 修饰的变量可以在 Vertex Shader 或 Fragment Shader 中使用,类似 C 语言中的 const,传入到 Shader 中是不能被修改的,一般用来表示变换矩阵。

attribute

attribute 修饰的变量是只能在 Vertex Shader 里面使用,一般用来表示一些顶点属性,通过函数 glBindAttribLocation 来绑定 attribute 修饰的变量的位置,然后再通过函数 glVertexAttribPointer 为其赋值。

varying

varying 修饰的变量是只能在 Shader 间里面使用,外部不能修改。上面也提到着色器之间是靠输入输出传递上下文,也就是靠 varying 修饰的变量作为桥梁。注意,它在 Shader 间的声明必须一致,而且在 iOS 中,需要说明精度信息。

动手改造

自定义 Shader

实际上我们这里使用的 Shader 的代码很简单,和使用 GLKBaseEffect 一样,我们需要传入顶点坐标。在这一节中,我们还加入了顶点颜色的属性。

顶点着色器

// l3vertex.vsh 
attribute vec4 a_position;
attribute vec4 a_color;

varying lowp vec4 frag_color;

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

2-3 行声明了我们会传入两个 4(float) 分量向量的顶点属性
5 行表明了着色器之间会通过 frag_color 传递上下文,如果这里不声明精度 lowp,编译时会报错
7-10 行则是我们 main 函数的代码
8 行声明了我们将输入的 a_color 赋值给 frag_color,而 frag_color 则会作为输出输入到 Fragment Shader 中
9 行则是将 a_position 赋值给系统的 gl_Position

片段着色器

// l3fragment.fsh
varying lowp vec4 frag_color;

void main(void) {
  gl_FragColor = frag_color;
}

同样,片段着色器的代码也和顶点着色器的代码类似,这里就不再累述。

实现一个 Custom BaseEffect

上面我们也提到 Shader 实际上是运行在 GPU 上的微程序,所以我们需要对输入的 GLSL 代码进行编译、链接。
首先,我们定义个叫 L3BaseEffect 的类,它的入参是 Shader 文件的名称(我们起码需要知道 GLSL 代码的文件名才能找到对应的代码进行编译)。

class L3BaseEffect {

    private var program: GLuint = 0

    init(_ vertexShaderName: String, _ fragmentShaderName: String) {
        self.compile(vertexShaderName, fragmentShaderName)
    }

    func prepareToDraw() {
        glUseProgram(program) // 设置 OpenGL 当前使用的 Program
    }
}

编译

当有了 GLSL 代码后,我们就可以对它们进行编译。下面都是相对固定的模板代码,就不多做解释。大部分代码都加了注释。

private func compileShader(_ shaderName: String, with type: GLenum) -> GLuint? {
    guard
        let shaderPath = Bundle.main.path(forResource: shaderName, ofType: nil),
        let shaderStr = try? String(contentsOfFile: shaderPath, encoding: .utf8)
        else {
            print("[Compile Shader Error]: Could Not Load The Shader")
            return nil
    }

    // 根据传入的 type 创建着色器对象
    // 这里的参数只能是 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER,分别代表顶点着色器和片段着色器
    let shader = glCreateShader(type)

    // 将传入的 GLSL 代码转换为 glShaderSource 能识别的参数
    let cShaderStr = shaderStr.cString(using: .utf8)
    var cShaderCount = GLint(Int32(cShaderStr!.count))
    var shaderSource = UnsafePointer<GLchar>(cShaderStr)

    // 关联着色器对象和着色器代码
    // 实际上就是关于 GLSL 代码的字符串到 shader 上
    glShaderSource(
        shader, // 需要关联的 shader
        1, // GLSL 代码字符串个数,一般就是 1 个
        &shaderSource, // GLSL 代码字符串
        &cShaderCount // GLSL 代码字符串长度
    )

    // 编译 shader
    glCompileShader(shader)

    var success = GLint()

    // 获取编译结果
    glGetShaderiv(shader, GLenum(GL_COMPILE_STATUS), &success)

    guard success != GL_FALSE else {
        // 输出错误信息
        var message: [GLchar] = []
        glGetShaderInfoLog(shader, GLsizei(MemoryLayout<GLchar>.size * 512), nil, &message)
        let messageStr = String(cString: message, encoding: .utf8)
        print("[Compile Shader Error]: \(String(describing: messageStr))")
        return nil
    }

    return shader
}

链接

链接实际上就是将上面编译好的 shader 附着在程序上,并且绑定对应的输入(相对于着色器的输入)。

private func compile(_ vertexShaderName: String, _ fragmentShaderName: String) {
    guard
        let vertexShader = compileShader(vertexShaderName, with: GLenum(GL_VERTEX_SHADER)),
        let fragmentShader = compileShader(fragmentShaderName, with: GLenum(GL_FRAGMENT_SHADER))
        else {
            print("[Link Program Error]: Could Not Compile Shader")
            return
    }

    // 创建着色器程序
    program = glCreateProgram()

    // 将着色器对象附加到着色器程序
    glAttachShader(program, vertexShader)
    glAttachShader(program, fragmentShader)

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

    // 链接程序
    glLinkProgram(program)

    var success = GLint()

    // 获取链接结果
    glGetProgramiv(program, GLenum(GL_LINK_STATUS), &success)

    if success == GL_FALSE {
        // 输出错误信息
        var message: [GLchar] = []
        glGetShaderInfoLog(program, GLsizei(MemoryLayout<GLchar>.size * 512), nil, &message)
        let messageStr = String(cString: message, encoding: .utf8)
        print("[Link Program Error]: \(String(describing: messageStr))")
    }
}

我们只要完成上面的代码,就相当于自己实现了简单的 BaseEffect 了。

替换 GLKBaseEffect

这里的代码替换很简单,只需要把原本有关 GLKBaseEffect 的代码都替换为 L3BaseEffect 即可。注意:因为我们没有使用 GLKBaseEffect,相应传输的顶点属性也记得修改。同时因为我们在这节还增加了 color 属性,记得在赋值 vertices 的时候也要加上。

// 注意: effect 的初始化需要在设置 EAGLContext.setCurrent 后
private lazy var effect: L3BaseEffect = L3BaseEffect("l3vertex.vsh", "l3fragment.fsh")

// 顶点数组
private let vertices: [L3Vertex] = [
    // 三角形
    L3Vertex(position: (0, 0.5, 0), color: (0, 0, 0, 1)),
    L3Vertex(position: (-0.5, -0.5, 0), color: (0, 0, 0, 1)),
    L3Vertex(position: (0.5, -0.5, 0), color: (0, 0, 0, 1))
]

private func setupVertexAttributes() {

    // 这里需要注意,因为我们已经切换为自定义的 BaseEffect,所以对应的顶点属性指也记得要修改
    glEnableVertexAttribArray(L3VertexAttrib.position.rawValue)

    // 传输坐标
    glVertexAttribPointer(
        L3VertexAttrib.position.rawValue,
        3,
        GLenum(GL_FLOAT),
        GLboolean(GL_FALSE),
        GLsizei(MemoryLayout<L3Vertex>.stride),
        UnsafeRawPointer(bitPattern: 0)
    )

    glEnableVertexAttribArray(L3VertexAttrib.color.rawValue)

    // 传输颜色
    glVertexAttribPointer(
        L3VertexAttrib.color.rawValue,
        4,
        GLenum(GL_FLOAT),
        GLboolean(GL_FALSE),
        GLsizei(MemoryLayout<L3Vertex>.stride),
        UnsafeRawPointer(bitPattern: 3 * MemoryLayout<GLfloat>.size)
    )
}

如果你对 glVertexAttribPointer 传参还有不明白的地方,可以看看下面这个图片。

效果

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

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

小结

  • 了解到如何自定义 Shader 以及一些 GLSL 的基础知识。

参考阅读

这是一个很好的 OpenGL 入门教程 Learn OpenGL CN

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