- 最终效果:
1. 流程总览
-
口述:
- 给 GPU 执行的代码叫做 Shader,我们写完 Shader 之后要告诉 GPU 使用它。
- 给 GPU 执行的数据,出生在内存,由我们 copy 到显存,并告诉 Shader 代码如何读取这些数据。
- FrameBuffer 是最终显示的内容,但它本身不保存内容,而是指向保存内容的 RenderBuffer (或 纹理)。
- GPU 的 Shader 代码处理显存数据得出的结果,存储在目标 RenderBuffer。
- 系统提供的窗口相关的方法和设置都在 Layer。
- 以上复杂关系由状态机 Context 进行管理,包括但不限于:
- GPU 用哪个 Shader
- FrameBuffer 指向哪个 RenderBuffer
- Shader 的执行结果存储在哪个 RenderBuffer
- Layer 何时切换显示数据 (FrameBuffer / RenderBuffer)
下图是 GLSL 加载图片的全流程 (建议下载原图食用),很实在的面向过程。看不懂,很正常,拿着这个图继续看到后面,应该会有点感觉。
2. 前置知识
2.1. Frame Buffer Object (FBO)
更详细见:帧缓存对象 Frame Buffer Object
作者:无名小基
https://blog.csdn.net/u014767384/article/details/81810388
Frame Buffer
可以意会为图像显示的最终表达者,但它本身不存储图像信息,而是指向存储图像信息的地方:Render Buffer
或 纹理。-
Render Buffer
和纹理都是分为3个类型:- 颜色
color
- 深度
depth
- 模板
stencil
- 颜色
-
以
Frame Buffer
关联Render Buffer
的函数为例,第1/3参数是不可改的,第4参数是Render Buffer
对象,第2参数是Frame Buffer
的附着点,有 颜色(0-15) / 深度(只有1个) / 模板(只有1个)。同一个
Frame Buffer
里所有的附着点的采样数必须一样。// 4. Framebuffer <=挂载 GL_RENDERBUFFER glFramebufferRenderbuffer( GLenum(GL_FRAMEBUFFER), //指定当前Framebuffer类型,其他2种:GL_DRAW_FRAMEBUFFER GL_READ_FRAMEBUFFER GLenum(GL_COLOR_ATTACHMENT0), //指定纹理对象在Framebuffer的挂载点,COLOR区可选0-15,其他3种:GL_DEPTH_ATTACHMENT GL_STENCIL_ATTACHMENT GL_DEPTH_STENCIL_ATTACHMENT GLenum(GL_RENDERBUFFER), //指定Renderbuffer类型,必须是GL_RENDERBUFFER myColorRenderBuffer) //指定Renderbuffer的对象
系统可同时存在多个
Frame Buffer
,能够随时切换不同的Frame Buffer
。然而,条件允许的话,性能更高的做法是Frame Buffer
指向不同的Render Buffer
或 纹理。
2.2. 图片解压缩
平时我们使用的 jpg png 是压缩过的图片,要转成位图才能作为纹理传入显存给GPU使用。代码很简单,就是分别设置好 原图/目标位图 的图片格式,然后调用系统方法 重新绘制一个位图。
3. Shader 文件
3.1. 创建
后缀名可以改,不影响执行
3.2. shaderv.vsh 顶点着色器
// attribute >> 顶点坐标
attribute vec4 position;
// attribute >> 纹理坐标
attribute vec2 texCoordinate;
// 顶点着色器 => 片元着色器的通道,vsh和fsh 里的声明必须完全相同
varying lowp vec2 varyTexCoord;
void main() {
// 纹理坐标 通过varyTexCoord 传递到 片元着色器
varyTexCoord = texCoordinate;
// 顶点坐标 (内建变量)
gl_Position = position;
}
3.3. shaderf.fsh 片元着色器
// float 使用高精度,下面如果有 float 使用低精度,必须加 lowp
precision highp float;
// 顶点着色器 => 片元着色器的通道,vsh和fsh 里的声明必须完全相同
varying lowp vec2 varyTexCoord;
// uniform >> 纹理
uniform sampler2D colorMap;
void main() {
// 1. 纹素:纹理在对应坐标的颜色值
lowp vec4 pixelTex = texture2D(colorMap, varyTexCoord);
// 2. 片元像素颜色 (内建变量)
gl_FragColor = pixelTex;
}
4. MyView.swift
4.1. 概览 (速记)
在override func layoutSubviews()
里使用 5步准备 + 1步绘制:
-
setupLayer()
将
self.layer
转成CAEAGLLayer
并保存为成员属性,来使用CALayer
没有的EAGLDrawable
接口 以及CAEAGLLayer
的一些方法。设置像素精度
self.contentScaleFactor
。-
设置绘制参数
myEAGLLayer.drawableProperties
:- 绘图表面显示后,是否保留其内容。
- 可绘制表面的内部颜色缓存区格式。
-
setupContext()
- 创建
EAGLContext
,并EAGLContext.setCurrent(xxx)
- 创建
-
deleteRenderAndFrameBuffer()
- 清空 RenderBuffer 和 FrameBuffer。
-
setupRenderBuffer()
var/Gen/Bind
通过context(内部调用了GL方法) 申请与Layer的显存空间,然后 绑定=> GL_RENDERBUFFER
-
setupFrameBuffer()
var/Gen/Bind
Framebuffer <=挂载 GL_RENDERBUFFER
-
renderLayer()
清屏颜色,清颜色缓冲区
设置视口
创建 program,shader编译后 附着到program,取得programID
链接 program
使用 program
准备顶点数据,内存 copy=> 显存:var/Gen/Bind/Data
设置某个attribute 读取 顶点坐标 的方式:Location/Enable/Pointer
设置某个attribute 读取 纹理坐标 的方式:Location/Enable/Pointer
加载纹理:图片 解压=> 位图 copy=> 显存的纹理,返回纹理ID
使用某uniform 传纹理:Location/Uniform
绘制:glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
RenderBuffer 显示=> 屏幕:myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))
4.2. 代码
import UIKit
@available(iOS, deprecated: 12.0)
class MDView: UIView {
var myEAGLLayer: CAEAGLLayer!
var myContext: EAGLContext!
var myColorRenderBuffer: GLuint = 0
var myColorFrameBuffer: GLuint = 0
var myProgram: GLuint = 0
// 顶点索引:bind之后对应 Shader的 Attribute/Uniform名
enum GLSLLocation {
enum Attribute: UInt32 {
case Position
case TexCoordinate
}
enum Uniform: UInt32 {
case ColorMap
}
}
override func layoutSubviews() {
// 5步准备工作
setupLayer()
setupContext()
deleteRenderAndFrameBuffer()
setupRenderBuffer()
setupFrameBuffer()
// 绘制
renderLayer()
}
// 如果不写,默认返回 CALayer.self,导致 myEAGLayer = layer as? CAEAGLLayer 失败
override class var layerClass: AnyClass {
return CAEAGLLayer.self
}
// 1. 设置图层
func setupLayer() {
// 1. 初始化 layer
myEAGLLayer = layer as? CAEAGLLayer
if myEAGLLayer == nil {
print("Convert Layer to CAEAGLLayer failed!!")
return
}
// 2. 设置 scale,1.0 或 2.0(此处)。
// 可以理解为像素精度,譬如某个view的size是50x50,scalefactor=2.0,位图要用到100x100像素。
contentScaleFactor = UIScreen.main.scale
// 3. 设置绘制参数 (总共也是2个):
myEAGLLayer.drawableProperties = [
// 绘图表面显示后,是否保留其内容。
kEAGLDrawablePropertyRetainedBacking : false,
// 可绘制表面的内部颜色缓存区格式
kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8
]
}
// 2. 设置上下文
func setupContext() {
myContext = EAGLContext(api: .openGLES3)
if myContext == nil {
print("Create EAGLContext failed!!")
return
}
guard EAGLContext.setCurrent(myContext) else {
print("Set current EAGLContext failed!!")
return
}
}
// 3. 清空缓冲区
func deleteRenderAndFrameBuffer() {
// 清空 RenderBuffer 和 FrameBuffer
glDeleteRenderbuffers(1, &myColorRenderBuffer)
myColorRenderBuffer = 0
glDeleteFramebuffers(1, &myColorFrameBuffer)
myColorFrameBuffer = 0
}
// 4. 创建/绑定 RenderBuffer
func setupRenderBuffer() {
// 1. 定义缓存区ID
var buffer: GLuint = 0
// 2. 申请缓存区标志
glGenRenderbuffers(1, &buffer)
myColorRenderBuffer = buffer
// 3. 标志(新申请的缓存区) 绑定=> GL_RENDERBUFFER
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
// 4. 通过context(内部调用了GL方法) 申请 Layer 匹配的显存空间,然后 绑定=> GL_RENDERBUFFER
myContext.renderbufferStorage(Int(GL_RENDERBUFFER), from: myEAGLLayer)
/*
第4步 替代了
glRenderbufferStorage(GLenum target,
GLenum internalformat,
GLsizei width,
GLsizei height)
2个方法都能用来申请显存空间,iOS方法通过EAGLLayer计算得来,而GL方法根据参数设置。
*/
}
// 5. FrameBuffer
func setupFrameBuffer() {
// 1. 定义缓存区ID
var buffer: GLuint = 0
// 2. 申请缓存区标志
glGenFramebuffers(1, &buffer)
myColorFrameBuffer = buffer
// 3. 标志(新申请的缓存区) 绑定=> GL_RENDERBUFFER
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), myColorFrameBuffer)
// 4. Framebuffer <=挂载 GL_RENDERBUFFER
glFramebufferRenderbuffer(
GLenum(GL_FRAMEBUFFER), //指定当前Framebuffer类型,其他2种:GL_DRAW_FRAMEBUFFER GL_READ_FRAMEBUFFER
GLenum(GL_COLOR_ATTACHMENT0), //指定纹理对象在Framebuffer的挂载点,COLOR区可选0-15,其他3种:GL_DEPTH_ATTACHMENT GL_STENCIL_ATTACHMENT GL_DEPTH_STENCIL_ATTACHMENT
GLenum(GL_RENDERBUFFER), //指定Renderbuffer类型,必须是GL_RENDERBUFFER
myColorRenderBuffer) //指定Renderbuffer的对象
}
// 6. 绘制
func renderLayer() {
// 1. 清屏颜色(浅红色),清颜色缓冲区
glClearColor(0.5, 0.1, 0.1, 0.5)
glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
// 2. 设置视口
let scale = UIScreen.main.scale
glViewport(GLint(frame.origin.x * scale),
GLint(frame.origin.y * scale),
GLsizei(frame.size.width * scale),
GLsizei(frame.size.height * scale))
// 3. 获取文件Bundle路径:顶点着色器/片元着色器
guard let vFilePath = Bundle.main.path(forAuxiliaryExecutable: "shaderv.vsh") else {
print("shaderv.vsh not found")
return
}
guard let fFilePath = Bundle.main.path(forAuxiliaryExecutable: "shaderf.fsh") else {
print("shaderf.fsh not found")
return
}
// 4. 创建program,取得programID:
// 4.1. filename => file => String => GLchar* 附着=> shader => shader编译
// 4.2. 创建program => vShader/fShader 分别附着=> program => 释放vShader/fShader
myProgram = loaderShaders(vShaderFilePath: vFilePath, fShaderFilePath: fFilePath)
/*
该步骤等同第10步,但必须在 glLinkProgram 之前执行
*/
glBindAttribLocation(
myProgram,
GLSLLocation.Attribute.TexCoordinate.rawValue,
"texCoordinate")
// 5. 链接 program
glLinkProgram(myProgram)
// 获取链接状态,做容错
var linkStatus: GLint = 0
glGetProgramiv(myProgram, GLenum(GL_LINK_STATUS), &linkStatus)
if linkStatus == GL_FALSE {
// Link 失败信息
var message = [GLchar](repeating: 96, count: 1024)
var length: GLsizei = 0
glGetProgramInfoLog(
myProgram,
GLsizei(MemoryLayout.stride(ofValue: message)), //存储log的buffer大小
&length, //获取到log信息的长度
&message) //获取到log信息
guard let messageStr = String(cString: message, encoding: .utf8) else {
print("Error when getting Link-failed log!!")
return
}
print(messageStr)
return
}
// 6. 使用 program,着色器代码准备就绪
glUseProgram(myProgram)
// 7. 顶点数据:顶点坐标/纹理坐标
let attrArr: [GLfloat] =
[
0.9, -0.3, -1.0, 1.0, 0.0,
-0.9, 0.3, -1.0, 0.0, 1.0,
-0.9, -0.3, -1.0, 0.0, 0.0,
0.9, 0.3, -1.0, 1.0, 1.0,
-0.9, 0.3, -1.0, 0.0, 1.0,
0.9, -0.3, -1.0, 1.0, 0.0,
]
GL_ELEMENT_ARRAY_BUFFER
// 8. 内存 copy=> 显存
var attrBuffer: GLuint = 0
glGenBuffers(1, &attrBuffer)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), attrBuffer)
glBufferData(
GLenum(GL_ARRAY_BUFFER),
MemoryLayout<GLfloat>.stride * attrArr.count,
attrArr,
GLenum(GL_STATIC_DRAW))
// 9. 设置某个attribute 读取 顶点坐标 的方式。
// 9.1. 获取program的某个attribute通道ID,这里是"position",要和shader文件里的命名完全一致。
let position = GLuint(glGetAttribLocation(myProgram, "position"))
// 9.2. 默认情况下,出于性能考虑 vshader的 attribute变量 都是关闭的,
// 即使数据到了GPU,如果不打开通道,该数据对 GPU(GPU的shader代码)就是不可见的。
// 该方法也可在glVertexAttribPointer之后调用,但必须早于 glDraw系列方法。
glEnableVertexAttribArray(position)
// 9.3. 设置读取方式
glVertexAttribPointer(
position, //index:数据通道的索引
3, //size:每个顶点用多少个数据描述,默认是4个
GLenum(GL_FLOAT), //type:数据类型,可用 GL_BYTE GL_SHORT,默认GL_FLOAT
GLboolean(GL_FALSE), //是否归一化 或 直接转换为固定值
GLsizei(MemoryLayout<GLfloat>.stride * 5), //stride:步长
UnsafePointer(bitPattern: 0)) //ptr:偏移
// 10. 设置某个attribute 读取 纹理坐标 的方式。同第9步。但本次使用 bind(第4/5步之间)
//let texCoordinate = GLuint(glGetAttribLocation(myProgram, "texCoordinate"))
//glEnableVertexAttribArray(texCoordinate)
glEnableVertexAttribArray(GLSLLocation.Attribute.TexCoordinate.rawValue)
glVertexAttribPointer(
GLSLLocation.Attribute.TexCoordinate.rawValue,
2,
GLenum(GL_FLOAT),
GLboolean(GL_FALSE),
GLsizei(MemoryLayout<GLfloat>.stride * 5),
UnsafePointer(bitPattern: MemoryLayout<GLfloat>.stride * 3))
// 11. 加载纹理:
// 图片 解压=> 位图 copy=> 显存的纹理,返回纹理ID
let textureID = setupTexture(fileName: "pcr.jpg")
// 12. 使用某uniform 传纹理,通道默认打开,不用去Enable
let colorMap = glGetUniformLocation(myProgram, "colorMap")
glUniform1i(
colorMap, //index:数据通道的索引
textureID) //纹理ID
// 13. 绘制:图元=三角形,从第0个顶点开始绘制,共6个顶点
glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
// 14.显示
myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))
}
/// 图片 解压=> 位图 copy=> 显存的纹理,返回纹理ID
/// - Parameter fileName: 图片文件名
/// - Returns: 纹理ID
func setupTexture(fileName: String) -> GLint {
// CGImage
guard let spriteImg = UIImage(named: fileName)?.cgImage else {
print("Fail to load image.")
return 0
}
// 原图的颜色空间
guard let colorSpace = spriteImg.colorSpace else {
print("Fail to get colorSpace.")
return 0
}
// 图片原宽高
let width = spriteImg.width
let height = spriteImg.height
// 每个像素颜色所需内存:RGBA(4位) 都占用 1个GLubyte
let pixelStride = MemoryLayout<GLubyte>.stride * 4
// 所有像素颜色所需内存
let totalStride = pixelStride * width * height
// 申请内存空间,重新绘制好的位图就保存在这里
let spriteData = UnsafeMutablePointer<GLubyte>.allocate(capacity: totalStride)
// https://developer.apple.com/documentation/coregraphics/cgcontext/1455939-init
// 创建 CGContext
let spriteContext = CGContext(
data: spriteData, //存储位置的引用,重新绘制好的位图就保存在这里
width: width, //将来位图的宽
height: height, //将来位图的高
bitsPerComponent: 8, //这次使用的32位的RGBA,每一位占用内存8位(是bit不是byte);若64则对应16
bytesPerRow: pixelStride * width, //每行占用内存n字节
space: colorSpace, //将来位图的颜色空间,不能是 索引颜色空间
bitmapInfo: spriteImg.bitmapInfo.rawValue) //alpha位的位置 & RGB位是否提前乘以alpha
// CGContext 绘图
let rect = CGRect(x: 0, y: 0, width: width, height: height)
spriteContext?.draw(spriteImg, in: rect)
// ----- 以上解压完成,下面把位图从 内存 copy=> 显存 -----
// 绑定纹理到ID
var textureID: GLuint = 0
glGenTextures(1, &textureID)
glBindTexture(GLenum(GL_TEXTURE_2D), textureID)
glActiveTexture(GLenum(GL_TEXTURE0 + Int32(textureID)))
/*
由于 ID = 0 的纹理区 默认一直处于激活状态,这里也只用到1个纹理ID,以上4行可以替换成:
glBindTexture(GLenum(GL_TEXTURE_2D), 0)
也因此,上面自己 glBindTexture的 textureID 总是从1开始,而不是0,
之后的 glActiveTexture 指定的 texturei 也要从 GL_TEXTURE0 开始加。
*/
// 设置纹理属性,缩小/放大的过滤方式,S/T轴的环绕方式
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
// 载入纹理:内存 copy=> 显存
glTexImage2D(
GLenum(GL_TEXTURE_2D), //纹理模式:1D 2D 3D
0, //level:指定详细程度编号,0 是基本图像级别,级别n是第n个缩略图缩小图像
GL_RGBA, //internalformat:纹理的内部样式,GL_ALPHA GL_LUMINANCE GL_LUMINANCE_ALPHA GL_RGB GL_RGBA
GLsizei(width), //纹理的宽
GLsizei(height), //纹理的高
0, //border:边框宽度,必须是0。
GLenum(GL_RGBA), //图像在内存的样式,大端/小端设备 分别对应 ARGB/BGRA,反正转换后必须与internalformat保持一致
GLenum(GL_UNSIGNED_BYTE), //图像在内存的数据类型
spriteData) //内存中存放图像数据的指针
// 释放 spriteData
free(spriteData)
return GLint(textureID)
}
// MARK:- shader
/// 用 顶点着色器/片元着色器 生成 program,返回 programID。
/// - Parameters:
/// - vShaderFilePath: 顶点着色器的Bundle文件路径
/// - fShaderFilePath: 片元着色器的Bundle文件路径
/// - Returns: programID
func loaderShaders(vShaderFilePath: String, fShaderFilePath: String) -> GLuint {
// 1. 定义 顶点着色器/片元着色器对象
var vShader: GLuint = 0
var fShader: GLuint = 0
// 2. 创建一个空的 program
let program = glCreateProgram()
// 3. 编译 顶点着色器/片元着色器
compileShader(shader: &vShader, type: GLenum(GL_VERTEX_SHADER), filePath: vShaderFilePath)
compileShader(shader: &fShader, type: GLenum(GL_FRAGMENT_SHADER), filePath: fShaderFilePath)
// 4. 顶点着色器/片元着色器 编译后的着色器代码 附着=> program
glAttachShader(program, vShader)
glAttachShader(program, fShader)
// 5. 释放 shader
glDeleteShader(vShader)
glDeleteShader(fShader)
return program
}
/// 把着色器源码绑定到shader,并编译成目标代码。
/// - Parameters:
/// - shader: id,编译完成后会被赋值
/// - type: 着色器类型
/// - file: 着色器文件名
func compileShader(shader: inout GLuint, type: GLenum, filePath: String) {
// 1. filePath => Data => String => [GLchar] => 指针
guard let content = FileManager.default.contents(atPath: filePath) else {
print("File not found!!")
return
}
guard let contentStr = String(data: content, encoding: .utf8) else {
print("Conver Data to String failed!!")
return
}
// String.cString() => [GLchar]
// typealias CChar = Int8
// typealias GLchar = Int8
guard let contentChar: [GLchar] = contentStr.cString(using: .utf8) else {
print("Conver String to [GLchar] failed!!")
return
}
/*
指针的变换:
[GLchar]
=> UnsafeMutablePointer<GLchar>
=> UnsafePointer<GLchar>?
这次直接使用符号 &,本质与封装2次指针相同 ↓
=> UnsafeMutablePointer<UnsafePointer<GLchar>?>
=> UnsafePointer<UnsafePointer<GLchar>?>
*/
let mutOncePointer = UnsafeMutablePointer<GLchar>.allocate(capacity: contentChar.count)
mutOncePointer.initialize(from: contentChar, count: contentChar.count)
var oncePointer = UnsafePointer?(mutOncePointer)
// 2. 创建一个对应类型的空的 shader对象
shader = glCreateShader(type)
// 3. 将着色器源码GLchar 附着=> shader对象
glShaderSource(shader, 1, &oncePointer, nil)
/*
以上 1.2.3.步骤有简略写法,参考自
https://blog.csdn.net/lin1109221208/article/details/107733156
里面的GitHub代码
*/
contentStr.withCString { (ccharPointer) in
var pointer: UnsafePointer<GLchar>? = ccharPointer
//glShaderSource(shader, 1, &pointer, nil)
}
// 4. 把着色器源码GLchar 编译成=> 目标代码
glCompileShader(shader)
}
}
5. Main.storyboard 文件
- view 的 Class 改为上面写的 MDView。