OpenGL ES 02 加载3张图片并做混合处理
- 什么是纹理单元
- 纹理单元的作用
- 使用纹理单元的步骤
- 详细解释
- 加载图片并绑定到到GPU纹理单元
- 采样器的设置
- 1.设置采样器变量的纹理单元编号,目的是为了告诉纹理采样器,从哪个纹理单元采集数据
- 2.如果你没有显式地设置采样器变量的纹理单元编号,OpenGL ES 可能会默认将所有采样器变量绑定到纹理单元 0。
- 3.这意味着所有的采样器变量(caodi, huangtu, noise)都会从纹理单元 0 中采样数据
- 顶点着色器(fragment.glsl)
- 片段着色器(fragment.glsl)
- Shader类封装
- OpenGL ES 页面整体代码
- `setupLayer` 方法
- `setupContext` 方法
- `setupRenderBuffers` 方法
- `setupFrameBuffer` 方法
- `prepareShader` 方法
- `prepareVAOAndVBO` 方法
- `loadImageToGPUTexture` 方法
- `renderLayer` 方法
- 总结
- 素材
- 最终效果
- 请添加图片描述
什么是纹理单元
纹理单元(Texture Unit)是 OpenGL 和 OpenGL ES 中的一个概念,用于管理和绑定多个纹理对象,以便在着色器中进行纹理采样操作。纹理单元允许你在一个渲染过程中使用多个纹理,而无需频繁地绑定和解绑纹理对象。
纹理单元的作用
-
管理多个纹理:
- 纹理单元允许你同时绑定多个纹理对象。每个纹理单元都有一个唯一的编号(索引),你可以通过这个编号来引用特定的纹理单元。
- 这使得在一个渲染过程中可以使用多个纹理,而无需频繁地绑定和解绑纹理对象。
-
在着色器中进行纹理采样:
- 在着色器中,你可以使用采样器(如
sampler2D
)从绑定到特定纹理单元的纹理对象中采样颜色数据。 - 通过将采样器变量与纹理单元绑定,着色器可以从不同的纹理单元中获取数据,从而实现复杂的纹理效果。
- 在着色器中,你可以使用采样器(如
使用纹理单元的步骤
加载Image图片,并绑定到对应的纹理单元:
func loadImageToGPUTexture(from path: String, index: Int) {
guard let image = UIImage(named: path)?.cgImage else {
fatalError("Failed to load image at path: \(path)")
}
let width = image.width
let height = image.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let rawData = calloc(height * width * 4, MemoryLayout<GLubyte>.size)
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width
let bitsPerComponent = 8
let context = CGContext(data: rawData,
width: width,
height: height,
bitsPerComponent: bitsPerComponent,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
var texture: GLuint = 0
glGenTextures(1, &texture)
glActiveTexture(GLenum(GL_TEXTURE0 + Int32(index)))
glBindTexture(GLenum(GL_TEXTURE_2D), texture)
glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), rawData)
free(rawData)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_NEAREST)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_REPEAT)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_REPEAT)
}
这个方法 loadImageToGPUTexture(from:index:)
的作用是将指定路径的图像加载到 GPU 的纹理单元中,以便在 OpenGL ES 中使用。以下是对该方法的详细解释,包括每个步骤的作用和流程。
详细解释
-
加载图像:
guard let image = UIImage(named: path)?.cgImage else { fatalError("Failed to load image at path: \(path)") }
- 使用
UIImage(named:)
加载指定路径的图像,并将其转换为CGImage
。 - 如果图像加载失败,程序将终止并输出错误信息。
- 使用
-
获取图像宽度和高度:
let width = image.width let height = image.height
- 获取图像的宽度和高度,以便后续使用。
-
创建颜色空间和原始数据缓冲区:
let colorSpace = CGColorSpaceCreateDeviceRGB() let rawData = calloc(height * width * 4, MemoryLayout<GLubyte>.size) let bytesPerPixel = 4 let bytesPerRow = bytesPerPixel * width let bitsPerComponent = 8
- 创建一个 RGB 颜色空间。
- 分配一个缓冲区
rawData
用于存储图像的原始像素数据。缓冲区大小为图像的宽度 * 高度 * 每像素字节数(4 字节,RGBA)。
-
创建 CGContext 并绘制图像:
let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
- 创建一个
CGContext
,用于将图像绘制到缓冲区rawData
中。 - 使用
context?.draw(image, in:)
将图像绘制到缓冲区中。
- 创建一个
-
生成和绑定纹理:
var texture: GLuint = 0 glGenTextures(1, &texture) glActiveTexture(GLenum(GL_TEXTURE0 + Int32(index))) glBindTexture(GLenum(GL_TEXTURE_2D), texture)
- 使用
glGenTextures
生成一个纹理对象,并将其 ID 存储在texture
变量中。 - 使用
glActiveTexture
激活指定的纹理单元(GL_TEXTURE0 + index
)。 - 使用
glBindTexture
将生成的纹理对象绑定到当前激活的纹理单元。
- 使用
-
上传纹理数据到 GPU:
glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), rawData)
- 使用
glTexImage2D
将缓冲区rawData
中的图像数据上传到 GPU 的纹理对象中。 - 参数说明:
GL_TEXTURE_2D
:目标纹理类型。0
:纹理的级别(通常为 0)。GL_RGBA
:纹理内部格式。width
和height
:纹理的宽度和高度。0
:边框宽度(必须为 0)。GL_RGBA
:像素数据的格式。GL_UNSIGNED_BYTE
:像素数据的类型。rawData
:指向像素数据的指针。
- 使用
-
释放原始数据缓冲区:
free(rawData)
- 释放之前分配的缓冲区
rawData
,以避免内存泄漏。
- 释放之前分配的缓冲区
-
设置纹理参数:
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR) glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_NEAREST) glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_REPEAT) glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_REPEAT)
- 使用
glTexParameteri
设置纹理参数:GL_TEXTURE_MAG_FILTER
:设置纹理放大时的过滤方式(GL_LINEAR
为线性过滤)。GL_TEXTURE_MIN_FILTER
:设置纹理缩小时的过滤方式(GL_NEAREST
为邻近过滤)。GL_TEXTURE_WRAP_S
和GL_TEXTURE_WRAP_T
:设置纹理在 S 和 T 方向上的环绕方式(GL_REPEAT
为重复)。
- 使用
加载图片并绑定到到GPU纹理单元
// 加载图片到GPU纹理单元0
loadImageToGPUTexture(from: "caodi", index: 0)
// 加载图片到GPU纹理单元1
loadImageToGPUTexture(from: "huangtu", index: 1)
// 加载图片到GPU纹理单元2
loadImageToGPUTexture(from: "noise", index: 2)
采样器的设置
1.设置采样器变量的纹理单元编号,目的是为了告诉纹理采样器,从哪个纹理单元采集数据
2.如果你没有显式地设置采样器变量的纹理单元编号,OpenGL ES 可能会默认将所有采样器变量绑定到纹理单元 0。
3.这意味着所有的采样器变量(caodi, huangtu, noise)都会从纹理单元 0 中采样数据
shader.setInt(name: "caodi", value: 0)
shader.setInt(name: "huangtu", value: 1)
shader.setInt(name: "noise", value: 2)
顶点着色器(fragment.glsl)
#version 300 es
// 接收顶点数据
layout (location = 0) in vec3 aPos; // 这里的location对应的是顶点属性的索引
layout (location = 1) in vec2 aUV; // 这里的location对应的是顶点属性的索引
out vec2 uv;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
uv = aUV;
}
片段着色器(fragment.glsl)
#version 300 es
precision mediump float;
out vec4 FragColor;
in vec2 uv;
// 纹理单元采样器, 必须要跟纹理单元对应上
uniform sampler2D caodi;
// 纹理单元采样器, 必须要跟纹理单元对应上
uniform sampler2D huangtu;
// 纹理单元采样, 必须要跟纹理单元对应上
uniform sampler2D noise;
void main()
{
// 从一张纹理图片中采样uv对应位置的颜色
vec4 caodiColor = texture(caodi, uv);
vec4 huangtuColor = texture(huangtu, uv);
vec4 noiceColor = texture(noise, uv);
// 将三张纹理图片的颜色混合
vec4 finalColor = mix(caodiColor, huangtuColor, noiceColor.r);
FragColor = vec4(finalColor.rgb, 1.0);
}
Shader类封装
定义了一个 Shader 类,用于加载、编译和链接 OpenGL 着色器程序,并提供了设置 uniform 变量和使用着色器程序的方法
import UIKit
class Shader: NSObject {
var shaderProgram: GLuint = 0
private func loadShaderSource(from file: String) -> String? {
guard let path = Bundle.main.path(forResource: file, ofType: "glsl") else {
print("Failed to find shader file: \(file)")
return nil
}
do {
let source = try String(contentsOfFile: path, encoding: .utf8)
return source
} catch {
print("Failed to load shader file: \(file), error: \(error)")
return nil
}
}
func setInt(name: String, value: Int) {
// 通过location设置uniform的值
glUniform1i(glGetUniformLocation(shaderProgram, name), GLint(value))
}
func begin() {
glUseProgram(shaderProgram)
}
func compileShader(vert: String, frag: String) {
// 读取着色器源代码
guard let vertexSource = loadShaderSource(from: vert),
let fragmentSource = loadShaderSource(from: frag)
else {
return
}
// 打印着色器源代码
print("Vertex Shader Source:\n\(vertexSource)")
print("Fragment Shader Source:\n\(fragmentSource)")
// 创建着色器程序
let vertexShader = glCreateShader(GLenum(GL_VERTEX_SHADER))
let fragmentShader = glCreateShader(GLenum(GL_FRAGMENT_SHADER))
// 将着色器源码附加到着色器对象上
vertexSource.withCString { ptr in
var p: UnsafePointer<GLchar>? = UnsafePointer<GLchar>(ptr)
glShaderSource(vertexShader, 1, &p, nil)
}
fragmentSource.withCString { ptr in
var p: UnsafePointer<GLchar>? = UnsafePointer<GLchar>(ptr)
glShaderSource(fragmentShader, 1, &p, nil)
}
// 编译顶点着色器
glCompileShader(vertexShader)
// 检查编译错误
var status: GLint = 0
glGetShaderiv(vertexShader, GLenum(GL_COMPILE_STATUS), &status)
if status == GL_FALSE {
var logLength: GLint = 0
glGetShaderiv(vertexShader, GLenum(GL_INFO_LOG_LENGTH), &logLength)
// Allocate buffer with an extra byte for the null terminator
let bufferLength = Int(logLength) + 1
var log = [GLchar](repeating: 0, count: bufferLength)
// Get the shader info log
glGetShaderInfoLog(vertexShader, logLength, nil, &log)
// Convert the buffer to a Swift string
if let logString = String(validatingUTF8: log) {
print("编译 顶点着色器 error: \(logString)")
} else {
print("编译 顶点着色器 error: Failed to retrieve log.")
}
return
}
// 编译片元着色器
glCompileShader(fragmentShader)
// 检查编译错误
glGetShaderiv(fragmentShader, GLenum(GL_COMPILE_STATUS), &status)
if status == GL_FALSE {
var logLength: GLint = 0
glGetShaderiv(fragmentShader, GLenum(GL_INFO_LOG_LENGTH), &logLength)
// Allocate buffer with an extra byte for the null terminator
let bufferLength = Int(logLength) + 1
var log = [GLchar](repeating: 0, count: bufferLength)
// Get the shader info log
glGetShaderInfoLog(fragmentShader, logLength, nil, &log)
// Convert the buffer to a Swift string
if let logString = String(validatingUTF8: log) {
print("编译 片元着色器 error: \(logString)")
} else {
print("编译 片元着色器 error: Failed to retrieve log.")
}
return
}
// 创建程序对象并链接着色器
shaderProgram = glCreateProgram()
glAttachShader(shaderProgram, vertexShader)
glAttachShader(shaderProgram, fragmentShader)
glLinkProgram(shaderProgram)
var linkStatus: GLint = 0
// 获取链接状态
glGetProgramiv(shaderProgram, GLenum(GL_LINK_STATUS), &linkStatus)
if linkStatus == GL_FALSE {
NSLog("link error")
let message = UnsafeMutablePointer<GLchar>.allocate(capacity: 512)
glGetProgramInfoLog(shaderProgram, GLsizei(MemoryLayout<GLchar>.size * 512), nil, message)
let str = String(utf8String: message)
print("error = \(str ?? "没获取到错误信息")")
return
} else {
NSLog("link success")
}
// 删除着色器对象,因为它们已经链接到程序对象中
glDeleteShader(vertexShader)
glDeleteShader(fragmentShader)
}
}
OpenGL ES 页面整体代码
//
// ViewController.swift
// OpenGLESLoadImage
//
// Created by anker on 2024/12/17.
//
import GLKit
import UIKit
/**
渲染一个图片纹理
*/
class ViewController: UIViewController {
var eaglLayer: CAEAGLLayer!
var myContext: EAGLContext!
var shader = Shader()
var vao = GLuint()
var renderBuffer = GLuint()
var frameBuffer = GLuint()
var fbo = GLuint()
var fboTexture = GLuint()
override func viewDidLoad() {
super.viewDidLoad()
// 设置渲染显示区域
setupLayer()
// 初始化上下文
setupContext()
// 设置帧缓冲区
setupRenderBuffers()
setupFrameBuffer()
// 准备着色器
prepareShader()
prepareVAOAndVBO()
// 加载图片到GPU纹理单元0
loadImageToGPUTexture(from: "caodi", index: 0)
// 加载图片到GPU纹理单元1
loadImageToGPUTexture(from: "huangtu", index: 1)
// 加载图片到GPU纹理单元2
loadImageToGPUTexture(from: "noise", index: 2)
renderLayer()
}
func setupLayer() {
eaglLayer = CAEAGLLayer()
eaglLayer.frame = view.frame
eaglLayer.isOpaque = true
view.layer.addSublayer(eaglLayer)
}
func setupContext() {
if let context = EAGLContext(api: .openGLES3) {
EAGLContext.setCurrent(context)
myContext = context
print("Create context success")
} else {
print("Create context failed!")
}
}
// 生成和绑定渲染缓冲区对象,为渲染缓冲区分配存储空间,生成和绑定帧缓冲区对象,并将渲染缓冲区附加到帧缓冲区的颜色附件点
func setupRenderBuffers() {
// 生成一个渲染缓冲区对象,并将其ID存储在,colorRenderBuffer变量中。
glGenRenderbuffers(1, &renderBuffer)
//将生成的渲染缓冲区对象绑定到当前的 OpenGL ES 渲染缓冲区目标,使其成为当前的渲染缓冲区。之后的渲染操作将会使用这个缓冲区。
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), renderBuffer)
}
func setupFrameBuffer() {
// 生成一个帧缓冲区对象,并将其 ID 存储在 frameBuffer 变量中。帧缓冲区对象是一个用于管理多个渲染缓冲区和纹理附件的对象。
glGenFramebuffers(1, &frameBuffer)
// 生成的帧缓冲区对象绑定到当前的 OpenGL ES 帧缓冲区目标,使其成为当前的帧缓冲区。之后的渲染操作将会使用这个帧缓冲区。
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), frameBuffer)
// 将之前生成并绑定的渲染缓冲区对象附加到当前帧缓冲区的颜色附件点。颜色附件点是帧缓冲区的一部分,用于存储颜色信息。
glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), renderBuffer)
// 方法为当前绑定的渲染缓冲区分配存储空间,并将其与 CAEAGLLayer 关联。
myContext.renderbufferStorage(Int(GL_RENDERBUFFER), from: eaglLayer)
}
func prepareShader() {
shader.compileShader(vert: "vertex", frag: "fragment")
}
func prepareVAOAndVBO() {
let positions: [GLfloat] = [
-1, -1, 0.0, // 左下角
1, -1, 0.0, // 右下角
-1, 1, 0.0, // 左上角
1, 1, 0.0 // 左上角
]
let colors: [GLfloat] = [
1.0, 0.2, 0.2, 1.0,
0.5, 1.0, 0.2, 1.0,
0.5, 0.5, 1.0, 1.0
]
// uvs 数据 2D纹理坐标,坐标系的左下角是(0, 0),右上角是(1, 1), 代表图片纹理的取值区域
let uvs: [GLfloat] = [
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
1.0, 1.0
]
let indices: [GLubyte] = [
0, 1, 2, 3
]
var positionVBO = GLuint()
glGenBuffers(1, &positionVBO)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), positionVBO)
glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * positions.count, positions, GLenum(GL_STATIC_DRAW))
var uvVBO = GLuint()
glGenBuffers(1, &uvVBO)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), uvVBO)
glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * uvs.count, uvs, GLenum(GL_STATIC_DRAW))
var ebo = GLuint()
glGenBuffers(1, &ebo)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), ebo)
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER), MemoryLayout<GLubyte>.size * indices.count, indices, GLenum(GL_STATIC_DRAW))
glGenVertexArrays(1, &vao)
glBindVertexArray(vao)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), positionVBO)
glVertexAttribPointer(0, 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 3), nil)
glEnableVertexAttribArray(0)
// uv
glBindBuffer(GLenum(GL_ARRAY_BUFFER), uvVBO)
glVertexAttribPointer(1, 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 2), nil)
glEnableVertexAttribArray(1)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), ebo)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
glBindVertexArray(0)
}
func loadImageToGPUTexture(from path: String, index: Int) {
guard let image = UIImage(named: path)?.cgImage else {
fatalError("Failed to load image at path: \(path)")
}
let width = image.width
let height = image.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let rawData = calloc(height * width * 4, MemoryLayout<GLubyte>.size)
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width
let bitsPerComponent = 8
let context = CGContext(data: rawData,
width: width,
height: height,
bitsPerComponent: bitsPerComponent,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
var texture: GLuint = 0
glGenTextures(1, &texture)
glActiveTexture(GLenum(GL_TEXTURE0 + Int32(index)))
glBindTexture(GLenum(GL_TEXTURE_2D), texture)
glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), rawData)
free(rawData)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_NEAREST)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_REPEAT)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_REPEAT)
}
func renderLayer() {
glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
glViewport(GLint(0), GLint(0), GLsizei(view.frame.size.width), GLsizei(view.frame.size.height))
shader.begin()
//解绑之前的帧缓冲区,恢复默认帧缓冲区
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)
//指定的帧缓冲区(Frame Buffer)绑定到当前的 OpenGL ES 渲染上下文中,使frameBuffer帧缓冲区其成为当前的渲染目标
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), frameBuffer)
//设置采样器变量的纹理单元编号,目的是为了告诉纹理采样器,从哪个纹理单元采集数据
//如果你没有显式地设置采样器变量的纹理单元编号,OpenGL ES 可能会默认将所有采样器变量绑定到纹理单元 0。
//这意味着所有的采样器变量(caodi, huangtu, noise)都会从纹理单元 0 中采样数据
shader.setInt(name: "caodi", value: 0)
shader.setInt(name: "huangtu", value: 1)
shader.setInt(name: "noise", value: 2)
// 绘制一个全屏四边形,将FBO纹理渲染到屏幕上
// 你需要设置适当的顶点和纹理坐标
// 这里假设你已经有一个VAO和VBO来绘制全屏四边形
glBindVertexArray(0)
glBindVertexArray(vao)
glDrawElements(GLenum(GL_TRIANGLE_STRIP), 4, GLenum(GL_UNSIGNED_BYTE), nil)
// 将渲染缓冲区的内容呈现到屏幕上
myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))
}
}
这个代码展示了如何在 iOS 上使用 OpenGL ES 3.0 渲染一个带有纹理的四边形。以下是对代码的详细分析,包括每个步骤的作用和流程。
setupLayer
方法
func setupLayer() {
eaglLayer = CAEAGLLayer()
eaglLayer.frame = view.frame
eaglLayer.isOpaque = true
view.layer.addSublayer(eaglLayer)
}
setupLayer
方法创建一个 CAEAGLLayer
,用于显示 OpenGL ES 渲染结果,并将其添加到视图的图层中。
setupContext
方法
func setupContext() {
if let context = EAGLContext(api: .openGLES3) {
EAGLContext.setCurrent(context)
myContext = context
print("Create context success")
} else {
print("Create context failed!")
}
}
setupContext
方法创建一个 OpenGL ES 3.0 渲染上下文,并将其设置为当前上下文。
setupRenderBuffers
方法
生成和绑定渲染缓冲区对象,为渲染缓冲区分配存储空间,生成和绑定帧缓冲区对象,并将渲染缓冲区附加到帧缓冲区的颜色附件点
func setupRenderBuffers() {
glGenRenderbuffers(1, &renderBuffer)
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), renderBuffer)
}
setupRenderBuffers
方法生成并绑定一个渲染缓冲区对象。
setupFrameBuffer
方法
func setupFrameBuffer() {
glGenFramebuffers(1, &frameBuffer)
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), frameBuffer)
glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), renderBuffer)
myContext.renderbufferStorage(Int(GL_RENDERBUFFER), from: eaglLayer)
}
setupFrameBuffer
方法生成并绑定一个帧缓冲区对象,并将渲染缓冲区附加到帧缓冲区的颜色附件点。
prepareShader
方法
func prepareShader() {
shader.compileShader(vert: "vertex", frag: "fragment")
}
prepareShader
方法编译和链接顶点着色器和片段着色器。
prepareVAOAndVBO
方法
func prepareVAOAndVBO() {
let positions: [GLfloat] = [
-1, -1, 0.0, // 左下角
1, -1, 0.0, // 右下角
-1, 1, 0.0, // 左上角
1, 1, 0.0 // 左上角
]
let colors: [GLfloat] = [
1.0, 0.2, 0.2, 1.0,
0.5, 1.0, 0.2, 1.0,
0.5, 0.5, 1.0, 1.0
]
let uvs: [GLfloat] = [
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
1.0, 1.0
]
let indices: [GLubyte] = [
0, 1, 2, 3
]
var positionVBO = GLuint()
glGenBuffers(1, &positionVBO)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), positionVBO)
glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * positions.count, positions, GLenum(GL_STATIC_DRAW))
var uvVBO = GLuint()
glGenBuffers(1, &uvVBO)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), uvVBO)
glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * uvs.count, uvs, GLenum(GL_STATIC_DRAW))
var ebo = GLuint()
glGenBuffers(1, &ebo)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), ebo)
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER), MemoryLayout<GLubyte>.size * indices.count, indices, GLenum(GL_STATIC_DRAW))
glGenVertexArrays(1, &vao)
glBindVertexArray(vao)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), positionVBO)
glVertexAttribPointer(0, 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 3), nil)
glEnableVertexAttribArray(0)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), uvVBO)
glVertexAttribPointer(1, 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 2), nil)
glEnableVertexAttribArray(1)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), ebo)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
glBindVertexArray(0)
}
prepareVAOAndVBO
方法创建并初始化顶点数组对象(VAO)、顶点缓冲对象(VBO)和元素缓冲对象(EBO),并将顶点数据、颜色数据和纹理坐标数据传输到 GPU。
loadImageToGPUTexture
方法
func loadImageToGPUTexture(from path: String, index: Int) {
guard let image = UIImage(named: path)?.cgImage else {
fatalError("Failed to load image at path: \(path)")
}
let width = image.width
let height = image.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let rawData = calloc(height * width * 4, MemoryLayout<GLubyte>.size)
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width
let bitsPerComponent = 8
let context = CGContext(data: rawData,
width: width,
height: height,
bitsPerComponent: bitsPerComponent,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
var texture: GLuint = 0
glGenTextures(1, &texture)
glActiveTexture(GLenum(GL_TEXTURE0 + Int32(index)))
glBindTexture(GLenum(GL_TEXTURE_2D), texture)
glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), rawData)
free(rawData)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_NEAREST)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_REPEAT)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_REPEAT)
}
loadImageToGPUTexture
方法将指定路径的图像加载到 GPU 的纹理单元中。具体步骤包括:
- 加载图像并获取其宽度和高度。
- 创建颜色空间和原始数据缓冲区。
- 创建
CGContext
并将图像绘制到缓冲区中。 - 生成并绑定纹理对象。
- 将图像数据上传到 GPU 的纹理对象中。
- 释放原始数据缓冲区。
- 设置纹理参数。
renderLayer
方法
func renderLayer() {
glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
glViewport(GLint(0), GLint(0), GLsizei(view.frame.size.width), GLsizei(view.frame.size.height))
shader.begin()
// 解绑之前的帧缓冲区,恢复默认帧缓冲区
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)
// 指定的帧缓冲区(Frame Buffer)绑定到当前的 OpenGL ES 渲染上下文中,使 frameBuffer 帧缓冲区其成为当前的渲染目标
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), frameBuffer)
// 设置采样器变量的纹理单元编号,目的是为了告诉纹理采样器,从哪个纹理单元采集数据
shader.setInt(name: "caodi", value: 0)
shader.setInt(name: "huangtu", value: 1)
shader.setInt(name: "noise", value: 2)
// 绘制一个全屏四边形,将 FBO 纹理渲染到屏幕上
glBindVertexArray(0)
glBindVertexArray(vao)
glDrawElements(GLenum(GL_TRIANGLE_STRIP), 4, GLenum(GL_UNSIGNED_BYTE), nil)
// 将渲染缓冲区的内容呈现到屏幕上
myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))
}
renderLayer
方法负责执行渲染操作并将结果显示到屏幕上。具体步骤包括:
- 清除颜色缓冲区。
- 设置视口。
- 启用着色器程序。
- 解绑之前的帧缓冲区,恢复默认帧缓冲区。
- 绑定帧缓冲区。
- 设置采样器变量的纹理单元编号。
- 绑定 VAO 并绘制全屏四边形。
- 将渲染缓冲区的内容呈现到屏幕上。
总结
这个代码展示了如何在 iOS 上使用 OpenGL ES 3.0 渲染一个带有纹理的四边形。具体步骤包括初始化 OpenGL ES 渲染环境、加载图像到 GPU 纹理单元、设置顶点数组对象和顶点缓冲对象、编译和链接着色器程序,以及执行渲染操作并将结果显示到屏幕上。通过这些步骤,图像数据被成功上传到 GPU,并可以在渲染过程中使用。
素材