前言
這是一篇iOS-Swift
版的OpenGL
入門教程晰房,環(huán)境 Xcode10 + OpenGL ES 3.0
编曼。iOS 12
之后已經(jīng)棄用 OpenGL ES
, 系統(tǒng)的一些框架全面改成默認(rèn)Metal
支持志衣。Metal
和 OpenGL ES、
Core Graphics屬于同一級(jí)的破停,也可以直接從
Metal學(xué)起骇扇,后續(xù)會(huì)推出
Metal`系列文章 。
構(gòu)建自己的GL ES View
為了更了解OpenGL ES 在iOS是怎么使用的间坐,這里拋棄了GLKit 提供 OpenGL ES 輔助的 GLKView灾挨。
1、首先創(chuàng)建 AGLKView 繼承自 UIView竹宋,這些變量后面會(huì)有介紹
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、默認(rèn)的 CALayer 是透明的蜈七,我們需要設(shè)置opaque
才能使其可見
fileprivate func setupLayer() {
let eagLayer = layer as? CAEAGLLayer
// CALayer 默認(rèn)是透明的秒拔,必須將它設(shè)為不透明才能讓其可見
eagLayer?.isOpaque = true
// 設(shè)置描繪屬性,在這里設(shè)置不維持渲染內(nèi)容以及顏色格式為 RGBA8
eagLayer?.drawableProperties = [kEAGLDrawablePropertyRetainedBacking:false,kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8]
}
4飒硅、至此 layer 的配置已經(jīng)就緒砂缩,下面我們來(lái)創(chuàng)建也設(shè)置 OpenGL ES 相關(guān)的東西。首先三娩,我們需要?jiǎng)?chuàng)建OpenGL ES 渲染上下文(在iOS 中對(duì)應(yīng)的實(shí)現(xiàn)為EAGLContext)庵芭,這個(gè) context 管理所有使用OpenGL ES 進(jìn)行描繪的狀態(tài),命令以及資源信息雀监。這與使用 Core Graphics 進(jìn)行描繪必須創(chuàng)建 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
}
// 設(shè)置為當(dāng)前上下文
if !EAGLContext.setCurrent(myContext) {
print("Failed to set current OpenGL context")
return
}
}
5会前、創(chuàng)建 RenderBuffer
與 Framebuffer
有了上下文好乐,OpenGL還需要在一塊 buffer 上進(jìn)行描繪,這塊 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)
// 為 顏色緩沖區(qū) 分配存儲(chǔ)空間
myContext?.renderbufferStorage(Int(GL_RENDERBUFFER), from: layer as? CAEAGLLayer)
glGenFramebuffers(1, &buffer)
myColorFrameBuffer = buffer
// 設(shè)置為當(dāng)前 framebuffer
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), myColorFrameBuffer)
// 將 _colorRenderBuffer 裝配到 GL_COLOR_ATTACHMENT0 這個(gè)裝配點(diǎn)上
glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
}
glGenRenderbuffers
原型為:
func glGenRenderbuffers(_ n: GLsizei, _ renderbuffers: UnsafeMutablePointer<GLuint>!)
它是為 renderbuffer
申請(qǐng)一個(gè)id。參數(shù)n 表示生成 renderbuffer 的個(gè)數(shù)临庇,而 renderbuffers 返回分配給 renderbuffer 的 id笛坦,注意:返回的 id 不會(huì)為0区转,id 0 是OpenGL ES 保留的,我們也不能使用 id 為0的 renderbuffer版扩。
glBindRenderbuffer
的原型為:
func glBindRenderbuffer(_ target: GLenum, _ renderbuffer: GLuint)
這個(gè)函數(shù)將指定 id 的 renderbuffer 設(shè)置為當(dāng)前 renderbuffer。參數(shù) target 必須為 GL_RENDERBUFFER侄泽,參數(shù) renderbuffer 是就是使用 glGenRenderbuffers 生成的 id礁芦。當(dāng)指定 id 的 renderbuffer 第一次被設(shè)置為當(dāng)前 renderbuffer 時(shí),會(huì)初始化該 renderbuffer 對(duì)象悼尾,其初始值為:
width
和 height
:像素單位的寬和高柿扣,默認(rèn)值為0;
internal format
:內(nèi)部格式闺魏,三大 buffer 格式之一 -- color未状,depth or stencil;
Color bit-depth
:僅當(dāng)內(nèi)部格式為 color 時(shí)析桥,設(shè)置顏色的 bit-depth司草,默認(rèn)值為0;
Depth bit-depth
:僅當(dāng)內(nèi)部格式為 depth時(shí)泡仗,默認(rèn)值為0埋虹;
Stencil bit-depth
: 僅當(dāng)內(nèi)部格式為 stencil,默認(rèn)值為0娩怎;
renderbufferStorage
原型為:
func renderbufferStorage(_ target: Int, from drawable: EAGLDrawable?) -> Bool
作用:將可繪制對(duì)象的存儲(chǔ)綁定到OpenGL ES renderbuffer對(duì)象搔课。參數(shù)target
必須為GL_RENDERBUFFER
。drawable
管理renderbuffer
的數(shù)據(jù)存儲(chǔ)的對(duì)象截亦,在iOS
中爬泥,此參數(shù)的值必須是一個(gè)CAEAGLLayer
對(duì)象。
創(chuàng)建Framebuffer Object
framebuffer object 通常也被稱之為 FBO崩瓤,它相當(dāng)于 buffer(color, depth, stencil)的管理者袍啡,三大buffer 可以附加到一個(gè) FBO 上。我們是用 FBO 來(lái)在 off-screen buffer上進(jìn)行渲染谷遂。
glFramebufferRenderbuffer
原型為:
func glFramebufferRenderbuffer(_ target: GLenum, _ attachment: GLenum, _ renderbuffertarget: GLenum, _ renderbuffer: GLuint)
該函數(shù)是將相關(guān) buffer(三大buffer之一)attach到framebuffer上(如果 renderbuffer不為 0葬馋,知道前面為什么說(shuō)glGenRenderbuffers 返回的id 不會(huì)為 0 吧)或從 framebuffer上detach(如果 renderbuffer為 0)。參數(shù) attachment 是指定 renderbuffer 被裝配到那個(gè)裝配點(diǎn)上肾扰,其值是GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT中的一個(gè)畴嘶,分別對(duì)應(yīng) color,depth和 stencil三大buffer集晚。
6窗悯、由于 layer 的寬高變化,導(dǎo)致原來(lái)創(chuàng)建的 renderbuffer不再相符偷拔,我們需要銷毀既有 renderbuffer 和 framebuffer蒋院。
fileprivate func destoryRenderAndFrameBuffer() {
// 當(dāng) UIView 在進(jìn)行布局變化之后亏钩,由于 layer 的寬高變化,導(dǎo)致原來(lái)創(chuàng)建的 renderbuffer不再相符欺旧,我們需要銷毀既有 renderbuffer 和 framebuffer姑丑。下面,我們依然創(chuàng)建私有方法 destoryRenderAndFrameBuffer 來(lái)銷毀生成的 buffer
glDeleteFramebuffers(1, &myColorFrameBuffer)
myColorFrameBuffer = 0
glDeleteRenderbuffers(1, &myColorRenderBuffer)
myColorRenderBuffer = 0
}
基本準(zhǔn)備工作完畢辞友,下面插播一些渲染理論知識(shí)栅哀。
渲染管線與著色器
管線(pipeline):OpenGL ES在渲染處理過(guò)程中會(huì)順序執(zhí)行一系列操作,這一系列相關(guān)的處理階段就被稱為OpenGL ES 渲染管線称龙。下圖就是OpenGL ES 的管線圖留拾。
圖中陰影部分的 Vertex Shader 和 Fragment Shader 就是可編程管線■曜穑可動(dòng)態(tài)編程實(shí)現(xiàn)這一功能一般都是腳本提供的痴柔,在OpenGL ES 中也一樣,編寫這樣腳本的能力是由著色語(yǔ)言GLSL提供的疫向。
1咳蔚、Shader (著色器語(yǔ)言)
著色語(yǔ)言是一種類C的編程語(yǔ)言,但不像C語(yǔ)言一樣支持雙精度浮點(diǎn)型(double)鸿捧、字節(jié)型(byte)屹篓、短整型(short)、長(zhǎng)整型(long)匙奴,并且取消了C中的聯(lián)合體(union)堆巧、枚舉類型(enum)、無(wú)符號(hào)數(shù)(unsigned)以及位運(yùn)算等特性泼菌。 著色語(yǔ)言中有許多內(nèi)建的原生數(shù)據(jù)類型以及構(gòu)建數(shù)據(jù)類型谍肤,如:浮點(diǎn)型(float)、布爾型(bool)哗伯、整型(int)荒揣、矩陣型(matrix)以及向量型(vec2、vec3等)等焊刹∠等危總體來(lái)說(shuō),這些數(shù)據(jù)類型可以分為標(biāo)量虐块、向量俩滥、矩陣、采樣器贺奠、結(jié)構(gòu)體以及數(shù)組等霜旧。 shader支持下面數(shù)據(jù)類型:
float bool int 基本數(shù)據(jù)類型
vec2 包含了2個(gè)浮點(diǎn)數(shù)的向量
vec3 包含了3個(gè)浮點(diǎn)數(shù)的向量
vec4 包含了4個(gè)浮點(diǎn)數(shù)的向量
ivec2 包含了2個(gè)整數(shù)的向量
ivec3 包含了3個(gè)整數(shù)的向量
ivec4 包含了4個(gè)整數(shù)的向量
bvec2 包含了2個(gè)布爾數(shù)的向量
bvec3 包含了3個(gè)布爾數(shù)的向量
bvec4 包含了4個(gè)布爾數(shù)的向量
mat2 2*2維矩陣
mat3 3*3維矩陣
mat4 4*4維矩陣
sampler1D 1D紋理采樣器
sampler2D 2D紋理采樣器
sampler3D 3D紋理采樣器
2、Vertex Shader (頂點(diǎn)著色器)
頂點(diǎn)著色器接收的輸入:
Attributes
:由 vertext array 提供的頂點(diǎn)數(shù)據(jù)儡率,如空間位置挂据,法向量以清,紋理坐標(biāo)以及頂點(diǎn)顏色,它是針對(duì)每一個(gè)頂點(diǎn)的數(shù)據(jù)崎逃。屬性只在頂點(diǎn)著色器中才有掷倔,片元著色器中沒(méi)有屬性。屬性可以理解為針對(duì)每一個(gè)頂點(diǎn)的輸入數(shù)據(jù)婚脱。
Uniforms
:uniforms保存由應(yīng)用程序傳遞給著色器的只讀常量數(shù)據(jù)今魔。在頂點(diǎn)著色器中,這些數(shù)據(jù)通常是變換矩陣障贸,光照參數(shù),顏色等吟宦。由 uniform 修飾符修飾的變量屬于全局變量篮洁,該全局性對(duì)頂點(diǎn)著色器與片元著色器均可見,也就是說(shuō)殃姓,這兩個(gè)著色器如果被連接到同一個(gè)應(yīng)用程序中袁波,它們共享同一份 uniform 全局變量集。因此如果在這兩個(gè)著色器中都聲明了同名的 uniform 變量蜗侈,要保證這對(duì)同名變量完全相同:同名+同類型篷牌,因?yàn)樗鼈儗?shí)際是同一個(gè)變量。
Samplers
:一種特殊的 uniform踏幻,用于呈現(xiàn)紋理枷颊。sampler 可用于頂點(diǎn)著色器和片元著色器。
Shader program
:由 main 申明的一段程序源碼该面,描述在頂點(diǎn)上執(zhí)行的操作:如坐標(biāo)變換夭苗,計(jì)算光照公式來(lái)產(chǎn)生 per-vertex 顏色或計(jì)算紋理坐標(biāo)。
頂點(diǎn)著色器的輸出:
Varying
:varying 變量用于存儲(chǔ)頂點(diǎn)著色器的輸出數(shù)據(jù)隔缀,當(dāng)然也存儲(chǔ)片元著色器的輸入數(shù)據(jù)题造,varying 變量最終會(huì)在光柵化處理階段被線性插值。頂點(diǎn)著色器如果聲明了 varying 變量猾瘸,它必須被傳遞到片元著色器中才能進(jìn)一步傳遞到下一階段界赔,因此頂點(diǎn)著色器中聲明的 varying 變量都應(yīng)在片元著色器中重新聲明同名同類型的 varying 變量。
在頂點(diǎn)著色器階段至少應(yīng)輸出位置信息-即內(nèi)建變量:gl_Position牵触,其它兩個(gè)可選的變量為:gl_FrontFacing 和 gl_PointSize淮悼。
示例代碼:
uniform mat4 uMVPMatrix; // 應(yīng)用程序傳入頂點(diǎn)著色器的總變換矩陣
attribute vec4 aPosition; // 應(yīng)用程序傳入頂點(diǎn)著色器的頂點(diǎn)位置
attribute vec2 aTextureCoord; // 應(yīng)用程序傳入頂點(diǎn)著色器的頂點(diǎn)紋理坐標(biāo)
attribute vec4 aColor // 應(yīng)用程序傳入頂點(diǎn)著色器的頂點(diǎn)顏色變量
varying vec4 vColor // 用于傳遞給片元著色器的頂點(diǎn)顏色數(shù)據(jù)
varying vec2 vTextureCoord; // 用于傳遞給片元著色器的頂點(diǎn)紋理數(shù)據(jù)
void main()
{
gl_Position = uMVPMatrix * aPosition; // 根據(jù)總變換矩陣計(jì)算此次繪制此頂點(diǎn)位置
vColor = aColor; // 將頂點(diǎn)顏色數(shù)據(jù)傳入片元著色器
vTextureCoord = aTextureCoord; // 將接收的紋理坐標(biāo)傳遞給片元著色器
}
3、Fragment Shader (片元著色器)
片元管理器接受如下輸入:
Varyings
:這個(gè)在前面已經(jīng)講過(guò)了荒吏,頂點(diǎn)著色器階段輸出的 varying 變量在光柵化階段被線性插值計(jì)算之后輸出到片元著色器中作為它的輸入敛惊,即上圖中的 gl_FragCoord,gl_FrontFacing 和 gl_PointCoord绰更。
Uniforms
:前面也已經(jīng)講過(guò)瞧挤,這里是用于片元著色器的常量锡宋,如霧化參數(shù),紋理參數(shù)等特恬;
Samples
:一種特殊的 uniform执俩,用于呈現(xiàn)紋理。
Shader program
:由 main 申明的一段程序源碼癌刽,描述在片元上執(zhí)行的操作役首。
在頂點(diǎn)著色器階段只有唯一的 varying 輸出變量-即內(nèi)建變量:gl_FragColor。
示例代碼:
precision mediump float; // 設(shè)置工作精度
varying vec4 vColor; // 接收從頂點(diǎn)著色器過(guò)來(lái)的頂點(diǎn)顏色數(shù)據(jù)
varying vec2 vTextureCoord; // 接收從頂點(diǎn)著色器過(guò)來(lái)的紋理坐標(biāo)
uniform sampler2D sTexture; // 紋理采樣器显拜,代表一幅紋理
void main()
{
gl_FragColor = texture2D(sTexture, vTextureCoord) * vColor;// 進(jìn)行紋理采樣
}
使用頂點(diǎn)著色器與片元著色器
接著前面的準(zhǔn)備工作衡奥,下面開始鏈接著色器。
主要步驟:
1远荠、創(chuàng)建 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 {
// 創(chuàng)建著色器對(duì)象
let shaderHandle = glCreateShader(type)
var shaderStringLength: GLint = GLint(Int32(shaderString.count))
var shaderCString = NSString(string: shaderString).utf8String
/* 把著色器源碼附加到著色器對(duì)象上
glShaderSource(shader: GLuint, count: GLsizei, String: UnsafePointer<UnsafePointer<GLchar>?>!, length: UnsafePointer<GLint>!)
shader: 著色器對(duì)象
count:指定要傳遞的源碼字符串?dāng)?shù)量档址,這里只有一個(gè)
String:著色器源碼
length:源碼長(zhǎng)度
*/
glShaderSource(shaderHandle, GLsizei(1), &shaderCString, &shaderStringLength)
// 編譯著色器
glCompileShader(shaderHandle)
// 編譯是否成功的狀態(tài) GL_FALSE GL_TRUE
var compileStatus: GLint = 0
// 獲取編譯狀態(tài)
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
// 獲取錯(cuò)誤消息
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
}
// 創(chuàng)建著色器程序?qū)ο? let programHandel = glCreateProgram()
if programHandel == 0 {return 0}
// 將著色器附加到程序上
glAttachShader(programHandel, vertexShader)
glAttachShader(programHandel, fragmentShader)
// 鏈接著色器程序
glLinkProgram(programHandel)
// 獲取鏈接狀態(tài)
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
// 獲取錯(cuò)誤消息
glGetProgramInfoLog(programHandel, bufferLenght, &actualLenght, UnsafeMutablePointer(mutating: info))
print("Error: Colorer Link Failed: \(String(validatingUTF8: info) ?? "")")
return 0
}
// 釋放資源
glDeleteShader(vertexShader)
glDeleteShader(fragmentShader)
return programHandel
}
}
編寫著色腳本
頂點(diǎn)著色器 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
]
// 加載頂點(diǎn)數(shù)據(jù)
glVertexAttribPointer(positionSlot, 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, vertices )
glEnableVertexAttribArray(positionSlot)
// 加載顏色數(shù)據(jù)
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 將在屏幕上的哪個(gè)區(qū)域呈現(xiàn)出來(lái),然后我們創(chuàng)建一個(gè)三角形頂點(diǎn)數(shù)組邻梆,通過(guò) glVertexAttribPointer 將三角形頂點(diǎn)數(shù)據(jù)裝載到 OpenGL ES 中并與 vPositon 關(guān)聯(lián)起來(lái)守伸,最后通過(guò) glDrawArrays 將三角形圖元渲染出來(lái)。
效果圖
這里可以下載demo代碼