OpenGL ES加载图片
2020-08-05 本文已影响0人
AndyGF
OpenGL ES 加载图片, 本文使用 GLSL 编写顶点着色器和片元着色器, 由于 Xcode 不支持 GLSL 语言的编译和链接, 所以需要自己手动去做这些事情.
原图如下, 四角上的坐标是我自己截图时加上去的, 对应的纹理坐标,
原图.png注意: 本文只是示例 demo, 如需用到项目中, 需要自己进行对应的优化.
主要分为以下6个关键步骤:
以下几个都是自定义函数
- 设置图层
setupLayer() - 设置图形上下文
setupContext() - 清空缓存区
deleteRenderAndFrameBuffer() - 设置RenderBuffer
setupRenderBuffer() - 设置FrameBuffer
setupFrameBuffer() - 开始绘制
renderLayer()
初始化
import UIKit
import OpenGLES
class GFShaderView: UIView {
//在iOS和tvOS上绘制OpenGL ES内容的图层,继承于 CALayer
private var myEaglLayer: CAEAGLLayer?
private var myContext: EAGLContext?
private var myColorRenderBuffer: GLuint = 0
private var myColorFrameBuffer: GLuint = 0
private var myPrograme: GLuint = 0
override init(frame: CGRect) {
super.init(frame: frame)
//1.设置图层
setupLayer()
//2.设置图形上下文
setupContext()
//3.清空缓存区
deleteRenderAndFrameBuffer()
//4.设置RenderBuffer
setupRenderBuffer()
//5.设置FrameBuffer
setupFrameBuffer()
//6.开始绘制
renderLayer()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override class var layerClass: AnyClass {
get {
return CAEAGLLayer.self
}
}
deinit {
// 删除两个缓冲区
glDeleteBuffers(1, &myColorFrameBuffer)
glDeleteBuffers(1, &myColorRenderBuffer)
}
}
1.设置图层
extension GFShaderView {
private func setupLayer() {
// 1.创建特殊图层
// 重写layerClass,将返回的图层 CALayer 替换成 CAEAGLLayer
myEaglLayer = layer as! CAEAGLLayer
// 2.设置scale
contentScaleFactor = UIScreen.main.scale
// 3.设置描述属性,这里设置不维持渲染内容以, 颜色格式为 RGBA8
myEaglLayer?.drawableProperties = [
kEAGLDrawablePropertyRetainedBacking: false,
kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
]
}
}
2.设置图形上下文
extension GFShaderView {
private func setupContext() {
// 设置版本
let api = EAGLRenderingAPI.openGLES3
// 2.创建上下文
guard let context = EAGLContext(api: api) else {
print("Create context failed!")
return
}
if !EAGLContext.setCurrent(context) {
print("setCurrentContext failed!");
return
}
// 4.赋值
myContext = context
}
}
3.清空缓存区
extension GFShaderView {
private func deleteRenderAndFrameBuffer() {
/*
buffer分为frame buffer 和 render buffer 2个大类。
其中frame buffer 相当于render buffer的管理者。
frame buffer object即称FBO。
render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
*/
glDeleteBuffers(1, &myColorRenderBuffer)
glDeleteBuffers(1, &myColorFrameBuffer)
myColorFrameBuffer = 0
myColorRenderBuffer = 0
}
}
4.设置RenderBuffer
extension GFShaderView {
private func setupRenderBuffer() {
// 1.定义一个缓存区ID
var buffer: GLuint = 0
// 2.申请一个标识符
glGenRenderbuffers(1, &buffer)
// 3.赋值
myColorRenderBuffer = buffer
// 4.将 render buffer 标识符绑定到 render buffer 对象
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
// 5.将可绘制对象 CAEAGLLayer 的存储绑定到 OpenGL ES renderBuffer 对象
myContext?.renderbufferStorage(Int(GL_RENDERBUFFER), from: myEaglLayer)
}
}
5.设置FrameBuffer
extension GFShaderView {
private func setupFrameBuffer() {
// 1.定义一个缓存区ID
var buffer: GLuint = 0
// 2.申请一个标识符
glGenRenderbuffers(1, &buffer)
// 3.赋值
myColorFrameBuffer = buffer
// 4.将 render buffer 标识符绑定到 render buffer 对象
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), myColorFrameBuffer)
/*
生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
*/
// 5.将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0 上。
glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
}
}
6.开始绘制
主要包括 OpenGL ES 上下文准备, 创建 programe, 加载着色器, 链接着色器, 处理顶点和纹理坐标, 加载纹理数据, 渲染绘制.
extension GFShaderView {
private func renderLayer() {
// 1.设置 背景色 / 缓冲区
// 清屏颜色
glClearColor(0.5, 0.5, 0.5, 1.0)
// 清空缓冲区
glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
// 2.设置视口大小
let scale = UIScreen.main.scale
let vx = GLint(frame.origin.x * scale)
let vy = GLint(frame.origin.y * scale)
let vw = GLsizei(frame.size.width * scale)
let vh = GLsizei(frame.size.height * scale)
glViewport(vx, vy, vw, vh)
// 3.读取顶点着色程序、片元着色程序
let vertFile = Bundle.main.path(forResource: "ShaderV", ofType: ".vsh") ?? ""
let fragFile = Bundle.main.path(forResource: "ShaderF", ofType: ".fsh") ?? ""
myPrograme = glCreateProgram()
// 4.加载 shader 程序 并绑定到 myPrograme 上
loadShaders(vertexShader: vertFile, fragmentShader: fragFile)
// 5.链接
glLinkProgram(myPrograme)
var linkStatus: GLint = -1
glGetProgramiv(myPrograme, GLenum(GL_LINK_STATUS), &linkStatus)
if linkStatus == GL_FALSE {
let message = UnsafeMutablePointer<GLchar>.allocate(capacity: 512)
let length = GLsizei(MemoryLayout<GLchar>.size * 512)
glGetProgramInfoLog(myPrograme, length, nil, message)
let messageString = String(utf8String: message) ?? ""
print("Program Link Error: \(messageString)")
return;
}
print("Program Link Success!")
// 6.使用 myPrograme
glUseProgram(myPrograme)
// 7.设置顶点 / 纹理坐标 (30 个元素)
let attrArr: [GLfloat] = [
0.5, -0.5, -1.0, 1.0, 0.0,
-0.5, 0.5, -1.0, 0.0, 1.0, //左上
-0.5, -0.5, -1.0, 0.0, 0.0, //左下
0.5, 0.5, -1.0, 1.0, 1.0, //右上
-0.5, 0.5, -1.0, 0.0, 1.0,
0.5, -0.5, -1.0, 1.0, 0.0, //右下
]
// 8.处理顶点数据 和 纹理坐标数据
var attrBuffer: GLuint = 0
// 申请一个缓存区标识符
glGenBuffers(1, &attrBuffer)
// 将 attrBuffer 绑定到 GL_ARRAY_BUFFER 标识符上
glBindBuffer(GLenum(GL_ARRAY_BUFFER), attrBuffer)
// 顶点数据 CPU 内存 copy 到 GPU 显存
glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * 30, attrArr, GLenum(GL_DYNAMIC_DRAW))
// 9.将顶点数据通过 myPrograme 中的传递到顶点着色程序的 position
let fSize = MemoryLayout<GLfloat>.size
let position = glGetAttribLocation(myPrograme, "position")
glEnableVertexAttribArray(GLuint(position))
glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(fSize * 5), UnsafeRawPointer(bitPattern: 0))
// 10.将纹理数据通过 myPrograme 中的传递到顶点着色程序的 textCoordinate
let textCoor = glGetAttribLocation(myPrograme, "textCoordinate")
glEnableVertexAttribArray(GLuint(textCoor))
glVertexAttribPointer(GLuint(textCoor), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(fSize * 5), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 3))
// 11.加载纹理
loadTexture("kunkun")
// 12.设置纹理采样器
let lc = glGetUniformLocation(myPrograme, "colorMap")
glUniform1i(lc, 0)
// 13.绘图
glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
// 14.从渲染缓存区显示到屏幕上
myContext?.presentRenderbuffer(Int(GL_RENDERBUFFER))
}
}
6.1加载 shader 程序
加载着色器代 > 编译 > 绑定到 myPrograme
// MARK: ---------- shader -----------
extension GFShaderView {
/// 加载 shader
private func loadShaders(vertexShader vFile: String, fragmentShader fFile: String) {
// 1.定义两个着色器
var verShader: GLuint = 0
var fragShader: GLuint = 0
// 2.编译顶点着色器 / 片元着色器
compileShader(shader: &verShader, type: GLenum(GL_VERTEX_SHADER), file: vFile)
compileShader(shader: &fragShader, type: GLenum(GL_FRAGMENT_SHADER), file: fFile)
// 3.创建最终的程序
glAttachShader(myPrograme, verShader)
glAttachShader(myPrograme, fragShader)
// 4.释放不需要的 shader
glDeleteShader(verShader)
glDeleteShader(fragShader)
}
/// 编译 shader
/// - Parameters:
/// - shader: 着色器
/// - type: 着色器类型
/// - file: 着色器代码文件的字符串
private func compileShader(shader: inout GLuint, type: GLenum, file: String) {
// 1.获取着色器程序源码文件中字符串
guard let content = try? String(contentsOfFile: file) else {
print("Get shader \(type) file failed")
return
}
// 2.创建一个 shader
shader = glCreateShader(type)
content.withCString {
var pointer: UnsafePointer<GLchar>? = $0
// 3.将源码附加到着色器上
//参数1:shader,要编译的着色器对象 *shader
//参数2:count,传递的源码字符串数量 1个
//参数3:string,着色器程序的源码(真正的着色器程序源码)
//参数4:length,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
glShaderSource(shader, 1, &pointer, nil)
}
// 4.把着色器代码编译成目标代码
glCompileShader(shader)
}
}
6.2.加载图片纹理
extension GFShaderView {
/// 从资源图片中加载纹理
/// - Parameter fileName: 图片名称
private func loadTexture(_ fileName: String) {
// 1.将 UIImage 转换为 CGImage
guard let spriteImage = UIImage(named: fileName)?.cgImage,
let space = spriteImage.colorSpace else {
print("Failed to load image \(fileName)")
exit(1)
}
// 2.读取图片的大小,宽和高
let spWidth = spriteImage.width
let spHeight = spriteImage.height
// 3.获取图片字节数 宽*高*4(RGBA)
let byteSize = MemoryLayout<GLubyte>.size
let spriteData = UnsafeMutablePointer<GLubyte>.allocate(capacity: byteSize * spWidth * spHeight * 4)
// 4.创建上下文CGImageAlphaPremultipliedLast
let spriteContext = CGContext(data: spriteData, width: spWidth, height: spHeight, bitsPerComponent: 8, bytesPerRow: spWidth * 4, space: space, bitmapInfo: 1)
// 5.在CGContextRef上--> 将图片绘制出来
let rect = CGRect(x: 0, y: 0, width: spWidth, height: spHeight)
// 6.使用默认方式绘制
spriteContext?.draw(spriteImage, in: rect)
// 7.绘制完成
UIGraphicsEndImageContext()
// 8.绑定纹理到默认的纹理ID
glBindTexture(GLenum(GL_TEXTURE_2D), 0)
// 9.设置纹理属性
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)
// 10.载入纹理 2D 数据
glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(spWidth), GLsizei(spHeight), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), spriteData)
// 11.释放 spriteData
free(spriteData)
}
}
7.顶点着色器
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main() {
varyTextCoord = textCoordinate;
gl_Position = position;
}
8.片元着色器
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main() {
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
执行起来的效果
程序渲染出来的样子
程序渲染出来的样子.png
我们想像中的样子
我们想像中的样子.png
如下图, 此时我们发现图片是倒置的, 为什么呢, 因为 OpenGL ES 和 纹理坐标系 y 轴正方向都向上, 而我们手机屏幕 y 轴 的正方向向下, 所以在就产生了上面的效果, 解决办法很简单, 也有很多种, 不同情况可以不同对待, 我采用调整纹理坐标对应顶点位置的方法解决倒置的问题,即调整为纹理左上对应顶点左下, 纹理右上对应顶点右下.
- 图片被倒置的原理解析图
调整纹理坐标
let attrArr: [GLfloat] = [
0.5, -0.5, -1.0, 1.0, 1.0,
-0.5, 0.5, -1.0, 0.0, 0.0, //左上
-0.5, -0.5, -1.0, 0.0, 1.0, //左下
0.5, 0.5, -1.0, 1.0, 0.0, //右上
-0.5, 0.5, -1.0, 0.0, 0.0,
0.5, -0.5, -1.0, 1.0, 1.0, //右下
]
- 调整纹理坐标对应的顶点坐标之后的对应关系图
在绘制之前对 context 进行平移, 缩放也是可以的, 由于缩放和平移的顺序不同, 所以参数也有所变化.
- 先平移, 再缩放
// 5.在CGContext上--> 将图片绘制出来
let rect = CGRect(x: 0, y: 0, width: spWidth, height: spHeight)
//通过翻转上下文方式, 将图片反转,
spriteContext?.translateBy(x: 0, y: CGFloat(spHeight))
spriteContext?.scaleBy(x: 1, y: -1)
// 6.使用默认方式绘制
spriteContext?.draw(spriteImage, in: rect)
- 先缩放, 再平移
// 5.在CGContext上--> 将图片绘制出来
let rect = CGRect(x: 0, y: 0, width: spWidth, height: spHeight)
//通过翻转上下文方式, 将图片反转,
spriteContext?.scaleBy(x: 1, y: -1)
spriteContext?.translateBy(x: 0, y: -CGFloat(spHeight))
// 6.使用默认方式绘制
spriteContext?.draw(spriteImage, in: rect)
着色器文件创建.png注意:
1.顶点着色器和片元着色器中不能有注释, 特别是中文注释, 有时候会报一些奇怪的错误, 而且很难查找.
2.着色器代码是写在空文件里的, 相当于字符串, Xcode 没有提示, 文件一般这样命名 xxx.vsh, xxx.fsh, .vsh 表示顶点着色器, .fsh 表示片元着色器,
3.着色器程序一定不能忘记写分号( ; ).
4.创建着色器文件时候用的空文件,文件名称和后缀都自己定.