前言
这是一篇iOS-Swift
版的OpenGL
入门教程,环境 Xcode10 + OpenGL ES 3.0
。iOS 12
之后已经弃用 OpenGL ES
, 系统的一些框架全面改成默认Metal
支持。Metal
和 OpenGL ES、
Core Graphics属于同一级的,也可以直接从
Metal学起,后续会推出
Metal`系列文章 。
构建自己的GL ES View
为了更了解OpenGL ES 在iOS是怎么使用的,这里抛弃了GLKit 提供 OpenGL ES 辅助的 GLKView。
1、首先创建 AGLKView 继承自 UIView,这些变量后面会有介绍
class AGLKView: UIView {
var myContext:EAGLContext?
var myColorFrameBuffer:GLuint = 0
var myColorRenderBuffer:GLuint = 0
var myProgram:GLuint?
var positionSlot:GLuint = 0
}
2、添加CAEAGLLayer
支持
// 只有CAEAGLLayer 类型的 layer 才支持 OpenGl 描绘
override class var layerClass : AnyClass {
return CAEAGLLayer.self
}
3、默认的 CALayer 是透明的,我们需要设置opaque
才能使其可见
fileprivate func setupLayer() {
let eagLayer = layer as? CAEAGLLayer
// CALayer 默认是透明的,必须将它设为不透明才能让其可见
eagLayer?.isOpaque = true
// 设置描绘属性,在这里设置不维持渲染内容以及颜色格式为 RGBA8
eagLayer?.drawableProperties = [kEAGLDrawablePropertyRetainedBacking:false,kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8]
}
4、至此 layer 的配置已经就绪,下面我们来创建也设置 OpenGL ES 相关的东西。首先,我们需要创建OpenGL ES 渲染上下文(在iOS 中对应的实现为EAGLContext),这个 context 管理所有使用OpenGL ES 进行描绘的状态,命令以及资源信息。这与使用 Core Graphics 进行描绘必须创建 Core Graphics Context 的道理是一样。
fileprivate func setupContext() {
// 指定 OpenGL 渲染 API 的版本,在这里我们使用 OpenGL ES 3.0
myContext = EAGLContext(api: .openGLES3)
if myContext == nil {
print("Failed to initialize OpenGLES 3.0 context")
return
}
// 设置为当前上下文
if !EAGLContext.setCurrent(myContext) {
print("Failed to set current OpenGL context")
return
}
}
5、创建 RenderBuffer
与 Framebuffer
有了上下文,OpenGL还需要在一块 buffer 上进行描绘,这块 buffer 就是 RenderBuffer(OpenGL ES 总共有三大不同用途的color buffer
,depth buffer
和 stencil buffer
,这里是最基本的 color buffer
)
fileprivate func setupBuffer() {
var buffer:GLuint = 0
glGenRenderbuffers(1, &buffer)
myColorRenderBuffer = buffer
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
// 为 颜色缓冲区 分配存储空间
myContext?.renderbufferStorage(Int(GL_RENDERBUFFER), from: layer as? CAEAGLLayer)
glGenFramebuffers(1, &buffer)
myColorFrameBuffer = buffer
// 设置为当前 framebuffer
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), myColorFrameBuffer)
// 将 _colorRenderBuffer 装配到 GL_COLOR_ATTACHMENT0 这个装配点上
glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
}
glGenRenderbuffers
原型为:
func glGenRenderbuffers(_ n: GLsizei, _ renderbuffers: UnsafeMutablePointer<GLuint>!)
它是为 renderbuffer
申请一个id。参数n 表示生成 renderbuffer 的个数,而 renderbuffers 返回分配给 renderbuffer 的 id,注意:返回的 id 不会为0,id 0 是OpenGL ES 保留的,我们也不能使用 id 为0的 renderbuffer。
glBindRenderbuffer
的原型为:
func glBindRenderbuffer(_ target: GLenum, _ renderbuffer: GLuint)
这个函数将指定 id 的 renderbuffer 设置为当前 renderbuffer。参数 target 必须为 GL_RENDERBUFFER,参数 renderbuffer 是就是使用 glGenRenderbuffers 生成的 id。当指定 id 的 renderbuffer 第一次被设置为当前 renderbuffer 时,会初始化该 renderbuffer 对象,其初始值为:
width
和 height
:像素单位的宽和高,默认值为0;
internal format
:内部格式,三大 buffer 格式之一 -- color,depth or stencil;
Color bit-depth
:仅当内部格式为 color 时,设置颜色的 bit-depth,默认值为0;
Depth bit-depth
:仅当内部格式为 depth时,默认值为0;
Stencil bit-depth
: 仅当内部格式为 stencil,默认值为0;
renderbufferStorage
原型为:
func renderbufferStorage(_ target: Int, from drawable: EAGLDrawable?) -> Bool
作用:将可绘制对象的存储绑定到OpenGL ES renderbuffer对象。参数target
必须为GL_RENDERBUFFER
。drawable
管理renderbuffer
的数据存储的对象,在iOS
中,此参数的值必须是一个CAEAGLLayer
对象。
创建Framebuffer Object
framebuffer object 通常也被称之为 FBO,它相当于 buffer(color, depth, stencil)的管理者,三大buffer 可以附加到一个 FBO 上。我们是用 FBO 来在 off-screen buffer上进行渲染。
glFramebufferRenderbuffer
原型为:
func glFramebufferRenderbuffer(_ target: GLenum, _ attachment: GLenum, _ renderbuffertarget: GLenum, _ renderbuffer: GLuint)
该函数是将相关 buffer(三大buffer之一)attach到framebuffer上(如果 renderbuffer不为 0,知道前面为什么说glGenRenderbuffers 返回的id 不会为 0 吧)或从 framebuffer上detach(如果 renderbuffer为 0)。参数 attachment 是指定 renderbuffer 被装配到那个装配点上,其值是GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT中的一个,分别对应 color,depth和 stencil三大buffer。
6、由于 layer 的宽高变化,导致原来创建的 renderbuffer不再相符,我们需要销毁既有 renderbuffer 和 framebuffer。
fileprivate func destoryRenderAndFrameBuffer() {
// 当 UIView 在进行布局变化之后,由于 layer 的宽高变化,导致原来创建的 renderbuffer不再相符,我们需要销毁既有 renderbuffer 和 framebuffer。下面,我们依然创建私有方法 destoryRenderAndFrameBuffer 来销毁生成的 buffer
glDeleteFramebuffers(1, &myColorFrameBuffer)
myColorFrameBuffer = 0
glDeleteRenderbuffers(1, &myColorRenderBuffer)
myColorRenderBuffer = 0
}
基本准备工作完毕,下面插播一些渲染理论知识。
渲染管线与着色器
管线(pipeline):OpenGL ES在渲染处理过程中会顺序执行一系列操作,这一系列相关的处理阶段就被称为OpenGL ES 渲染管线。下图就是OpenGL ES 的管线图。
pipeline.png
图中阴影部分的 Vertex Shader 和 Fragment Shader 就是可编程管线。可动态编程实现这一功能一般都是脚本提供的,在OpenGL ES 中也一样,编写这样脚本的能力是由着色语言GLSL提供的。
1、Shader (着色器语言)
着色语言是一种类C的编程语言,但不像C语言一样支持双精度浮点型(double)、字节型(byte)、短整型(short)、长整型(long),并且取消了C中的联合体(union)、枚举类型(enum)、无符号数(unsigned)以及位运算等特性。 着色语言中有许多内建的原生数据类型以及构建数据类型,如:浮点型(float)、布尔型(bool)、整型(int)、矩阵型(matrix)以及向量型(vec2、vec3等)等。总体来说,这些数据类型可以分为标量、向量、矩阵、采样器、结构体以及数组等。 shader支持下面数据类型:
float bool int 基本数据类型
vec2 包含了2个浮点数的向量
vec3 包含了3个浮点数的向量
vec4 包含了4个浮点数的向量
ivec2 包含了2个整数的向量
ivec3 包含了3个整数的向量
ivec4 包含了4个整数的向量
bvec2 包含了2个布尔数的向量
bvec3 包含了3个布尔数的向量
bvec4 包含了4个布尔数的向量
mat2 2*2维矩阵
mat3 3*3维矩阵
mat4 4*4维矩阵
sampler1D 1D纹理采样器
sampler2D 2D纹理采样器
sampler3D 3D纹理采样器
2、Vertex Shader (顶点着色器)
顶点着色器接收的输入:
Attributes
:由 vertext array 提供的顶点数据,如空间位置,法向量,纹理坐标以及顶点颜色,它是针对每一个顶点的数据。属性只在顶点着色器中才有,片元着色器中没有属性。属性可以理解为针对每一个顶点的输入数据。
Uniforms
:uniforms保存由应用程序传递给着色器的只读常量数据。在顶点着色器中,这些数据通常是变换矩阵,光照参数,颜色等。由 uniform 修饰符修饰的变量属于全局变量,该全局性对顶点着色器与片元着色器均可见,也就是说,这两个着色器如果被连接到同一个应用程序中,它们共享同一份 uniform 全局变量集。因此如果在这两个着色器中都声明了同名的 uniform 变量,要保证这对同名变量完全相同:同名+同类型,因为它们实际是同一个变量。
Samplers
:一种特殊的 uniform,用于呈现纹理。sampler 可用于顶点着色器和片元着色器。
Shader program
:由 main 申明的一段程序源码,描述在顶点上执行的操作:如坐标变换,计算光照公式来产生 per-vertex 颜色或计算纹理坐标。
顶点着色器的输出:
Varying
:varying 变量用于存储顶点着色器的输出数据,当然也存储片元着色器的输入数据,varying 变量最终会在光栅化处理阶段被线性插值。顶点着色器如果声明了 varying 变量,它必须被传递到片元着色器中才能进一步传递到下一阶段,因此顶点着色器中声明的 varying 变量都应在片元着色器中重新声明同名同类型的 varying 变量。
在顶点着色器阶段至少应输出位置信息-即内建变量:gl_Position,其它两个可选的变量为:gl_FrontFacing 和 gl_PointSize。
示例代码:
uniform mat4 uMVPMatrix; // 应用程序传入顶点着色器的总变换矩阵
attribute vec4 aPosition; // 应用程序传入顶点着色器的顶点位置
attribute vec2 aTextureCoord; // 应用程序传入顶点着色器的顶点纹理坐标
attribute vec4 aColor // 应用程序传入顶点着色器的顶点颜色变量
varying vec4 vColor // 用于传递给片元着色器的顶点颜色数据
varying vec2 vTextureCoord; // 用于传递给片元着色器的顶点纹理数据
void main()
{
gl_Position = uMVPMatrix * aPosition; // 根据总变换矩阵计算此次绘制此顶点位置
vColor = aColor; // 将顶点颜色数据传入片元着色器
vTextureCoord = aTextureCoord; // 将接收的纹理坐标传递给片元着色器
}
3、Fragment Shader (片元着色器)
片元管理器接受如下输入:
Varyings
:这个在前面已经讲过了,顶点着色器阶段输出的 varying 变量在光栅化阶段被线性插值计算之后输出到片元着色器中作为它的输入,即上图中的 gl_FragCoord,gl_FrontFacing 和 gl_PointCoord。
Uniforms
:前面也已经讲过,这里是用于片元着色器的常量,如雾化参数,纹理参数等;
Samples
:一种特殊的 uniform,用于呈现纹理。
Shader program
:由 main 申明的一段程序源码,描述在片元上执行的操作。
在顶点着色器阶段只有唯一的 varying 输出变量-即内建变量:gl_FragColor。
示例代码:
precision mediump float; // 设置工作精度
varying vec4 vColor; // 接收从顶点着色器过来的顶点颜色数据
varying vec2 vTextureCoord; // 接收从顶点着色器过来的纹理坐标
uniform sampler2D sTexture; // 纹理采样器,代表一幅纹理
void main()
{
gl_FragColor = texture2D(sTexture, vTextureCoord) * vColor;// 进行纹理采样
}
使用顶点着色器与片元着色器
接着前面的准备工作,下面开始链接着色器。
主要步骤:
1、创建 shader:glCreateShader
2、装载 shader:glShaderSource
3、编译 shader:glCompileShader
4、删除 shader:glDeleteShader 释放资源
着色器加载封装
struct GLESUtils {
static func loadShaderFile(type:GLenum, fileName:String) -> GLuint {
guard let path = Bundle.main.path(forResource: fileName, ofType: nil) else {
print("Error: file does not exist !")
return 0;
}
do {
let shaderString = try String(contentsOfFile: path, encoding: .utf8)
return GLESUtils.loadShaderString(type: type, shaderString: shaderString)
} catch {
print("Error: loading shader file: \(path)")
return 0;
}
}
static func loadShaderString(type:GLenum, shaderString:String) ->GLuint {
// 创建着色器对象
let shaderHandle = glCreateShader(type)
var shaderStringLength: GLint = GLint(Int32(shaderString.count))
var shaderCString = NSString(string: shaderString).utf8String
/* 把着色器源码附加到着色器对象上
glShaderSource(shader: GLuint, count: GLsizei, String: UnsafePointer<UnsafePointer<GLchar>?>!, length: UnsafePointer<GLint>!)
shader: 着色器对象
count:指定要传递的源码字符串数量,这里只有一个
String:着色器源码
length:源码长度
*/
glShaderSource(shaderHandle, GLsizei(1), &shaderCString, &shaderStringLength)
// 编译着色器
glCompileShader(shaderHandle)
// 编译是否成功的状态 GL_FALSE GL_TRUE
var compileStatus: GLint = 0
// 获取编译状态
glGetShaderiv(shaderHandle, GLenum(GL_COMPILE_STATUS), &compileStatus)
if compileStatus == GL_FALSE {
var infoLength: GLsizei = 0
let bufferLength: GLsizei = 1024
glGetShaderiv(shaderHandle, GLenum(GL_INFO_LOG_LENGTH), &infoLength)
let info: [GLchar] = Array(repeating: GLchar(0), count: Int(bufferLength))
var actualLength: GLsizei = 0
// 获取错误消息
glGetShaderInfoLog(shaderHandle, bufferLength, &actualLength, UnsafeMutablePointer(mutating: info))
NSLog(String(validatingUTF8: info)!)
print("Error: Colourer Compilation Failure: \(String(validatingUTF8: info) ?? "")")
return 0
}
return shaderHandle
}
static func loanProgram(verShaderFileName:String,fragShaderFileName:String) -> GLuint {
let vertexShader = GLESUtils.loadShaderFile(type: GLenum(GL_VERTEX_SHADER), fileName: verShaderFileName)
if vertexShader == 0 {return 0}
let fragmentShader = GLESUtils.loadShaderFile(type: GLenum(GL_FRAGMENT_SHADER), fileName: fragShaderFileName)
if fragmentShader == 0 {
glDeleteShader(vertexShader)
return 0
}
// 创建着色器程序对象
let programHandel = glCreateProgram()
if programHandel == 0 {return 0}
// 将着色器附加到程序上
glAttachShader(programHandel, vertexShader)
glAttachShader(programHandel, fragmentShader)
// 链接着色器程序
glLinkProgram(programHandel)
// 获取链接状态
var linkStatus: GLint = 0
glGetProgramiv(programHandel, GLenum(GL_LINK_STATUS), &linkStatus)
if linkStatus == GL_FALSE {
var infoLength: GLsizei = 0
let bufferLenght: GLsizei = 1024
glGetProgramiv(programHandel, GLenum(GL_INFO_LOG_LENGTH), &infoLength)
let info: [GLchar] = Array(repeating: GLchar(0), count: Int(bufferLenght))
var actualLenght: GLsizei = 0
// 获取错误消息
glGetProgramInfoLog(programHandel, bufferLenght, &actualLenght, UnsafeMutablePointer(mutating: info))
print("Error: Colorer Link Failed: \(String(validatingUTF8: info) ?? "")")
return 0
}
// 释放资源
glDeleteShader(vertexShader)
glDeleteShader(fragmentShader)
return programHandel
}
}
编写着色脚本
顶点着色器 shaderv.glsl
attribute vec4 vPosition;
attribute vec4 a_Color;
varying lowp vec4 frag_Color;
void main(void)
{
frag_Color = a_Color;
gl_Position = vPosition;
}
片元着色器shaderf.glsl
varying lowp vec4 frag_Color;
void main()
{
gl_FragColor = frag_Color;
}
编译着色器
fileprivate func setupProgram() {
myProgram = GLESUtils.loanProgram(verShaderFileName: "shaderv.glsl", fragShaderFileName: "shaderf.glsl")
guard let myProgram = myProgram else {
return
}
glUseProgram(myProgram)
positionSlot = GLuint(glGetAttribLocation(myProgram, "vPosition"))
colorSlot = GLuint(glGetAttribLocation(myProgram, "a_Color"))
}
绘制
fileprivate func render() {
glClearColor(0, 1.0, 0, 1.0)
glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
glViewport(0, 0, GLsizei(frame.size.width), GLsizei(frame.size.height))
let vertices: [GLfloat] = [
0, 0.5, 0,
-0.5, -0.5, 0,
0.5, -0.5, 0
]
let colors: [GLfloat] = [
1, 0, 0, 1,
0, 1, 0, 1,
0, 0, 1, 1
]
// 加载顶点数据
glVertexAttribPointer(positionSlot, 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, vertices )
glEnableVertexAttribArray(positionSlot)
// 加载颜色数据
glVertexAttribPointer(colorSlot, 4, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, colors )
glEnableVertexAttribArray(colorSlot)
// 绘制
glDrawArrays(GLenum(GL_TRIANGLES), 0, 3)
myContext?.presentRenderbuffer(Int(GL_RENDERBUFFER))
}
glViewport 表示渲染 surface 将在屏幕上的哪个区域呈现出来,然后我们创建一个三角形顶点数组,通过 glVertexAttribPointer 将三角形顶点数据装载到 OpenGL ES 中并与 vPositon 关联起来,最后通过 glDrawArrays 将三角形图元渲染出来。
效果图
效果图.png这里可以下载demo代码
网友评论