初學OpenGL ES,使用swift時有些地方需要注意尤其是C的指針代碼在 swift中的使用,eg:基礎指針UnsafeRawPointer,類型指針UnsafeMutablePointer<GLubyte>,
獲取類型的大小方法MemoryLayout<GLfloat>.size 本文中都有使用到,對於學OpenGL ES 的小伙伴可以借鑒,有問題也可隨時交流,本文還介紹了幾種紋理反轉的方法。
本片主要介紹使用編譯鏈接自定義的shader。用簡單的glsl語言來實現頂點、片元着色器,並對圖形進行簡單的變換。
OpenGLES只是用來做渲染的,所iOS要提供一個載體,就是CAEAGLLayer,創建的方法是,通過重寫UIView的類屬性(OC中是累方法)返回CAEAGLLayer.self。它是一個對core animation的封裝,它能滿足所有的OpenGLES的方法訪問。
- 關於CAEAGLLayer
在制定該圖層關聯的視圖作為渲染器的目標圖形上下文之前,可以使用drawableProperties屬性更改呈現屬性。此屬性允許您配置呈現表面的顏色格式以及表面是否保留其內容。 因為OpenGL ES渲染的效果是要提交到用戶使用的核心動畫上,所以你使用在該layer上的任何效果和動畫都會影響你渲染的3D效果,為了時性能最佳你應該做一下操作:設置圖層為不透明,設置圖層邊界以匹配顯示的尺寸,確保圖層沒有做變換。
盡量避免在CAEAGLLayer添加其layer。如果必須要添加其他非OpenGL內容,那么如果你將透明的2D內容置於GL內容之上,並確保OpenGL內容是不透明的且沒有轉換過,那么性能還是可以接受的。當在豎屏上繪制橫向內容時,你應該自己旋轉內容,而不是使用CAEAGLLayer轉換來旋轉它。
- 使用OpenGL ES 渲染一張圖片主要總結為一下步驟:
1.創建圖層
2.創建上下文
3.清空緩存區
4.設置RenderBuffer
5.設置FrameBuffer
6.開始繪制,此步驟中包含編譯連接使用着色器程序,以及加載紋理圖片
7.析構函數中釋放buffer
1.設置圖層
kEAGLDrawablePropertyRetainedBacking 表示繪圖表面顯示后,是否保留其內容。
kEAGLDrawablePropertyColorFormat
可繪制表面的內部顏色緩存區格式,這個key對應的值是一個NSString指定特定顏色緩存區對象。默認是kEAGLColorFormatRGBA8;
kEAGLColorFormatRGBA8:32位RGBA的顏色,4*8=32位
kEAGLColorFormatRGB565:16位RGB的顏色
kEAGLColorFormatSRGBA8:sRGB代表了標准的紅、綠、藍,即CRT顯示器、LCD顯示器、投影機、打印機以及其他設備中色彩再現所使用的三個基本色素。sRGB的色彩空間基於獨立的色彩坐標,可以使色彩在不同的設備使用傳輸中對應於同一個色彩坐標體系,而不受這些設備各自具有的不同色彩坐標的影響。
func createGLLayer() { glLayer = (self.layer as! CAEAGLLayer) self.contentScaleFactor = UIScreen.main.scale glLayer.drawableProperties = [kEAGLDrawablePropertyRetainedBacking : false,kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8] } //重寫父類類屬性layerClass,將View返回的圖層從CALayer替換成CAEAGLLayer override class var layerClass: AnyClass{ return CAEAGLLayer.self }
2.設置圖形上下文
1).指定OpenGL ES 渲染API版本,我們使用3.0,2.0和3.0差不多
2).創建圖形上下文
3).判斷是否創建成功
4).設置圖形上下文
func setUpContext(){ if let con = EAGLContext(api: EAGLRenderingAPI.openGLES3){ EAGLContext.setCurrent(con) self.context = con }else{ print("創建context失敗") } }
3.清除緩沖區
buffer分為frame buffer 和 render buffer2個大類。
其中frame buffer 相當於render buffer的管理者。
frame buffer object即稱FBO。
render buffer則又可分為3類。colorBuffer、depthBuffer、stencilBuffer。
func clearRenderBufferAndFrameBuffer() { glDeleteBuffers(1, &colorRederBuffer) colorRederBuffer = 0 glDeleteBuffers(1, &colorFrameBuffer) colorFrameBuffer = 0 }
4.設置渲染緩沖區
func setUpRenderBuffer() { //申請一個緩沖區標志 glGenRenderbuffers(1, &colorRederBuffer) //將標識符綁定到GL_RENDERBUFFER glBindRenderbuffer(GLenum(GL_RENDERBUFFER), colorRederBuffer) //將可繪制對象drawable object's CAEAGLLayer的存儲綁定到OpenGL ES renderBuffer對象 context.renderbufferStorage(Int(GLenum(GL_RENDERBUFFER)), from: glLayer) }
5.設置幀緩沖區
生成幀緩存區之后,則需要將renderbuffer跟framebuffer進行綁定,
調用glFramebufferRenderbuffer函數進行綁定到對應的附着點上,后面的繪制才能起作用
func setUpFrameBuffer() { //申請一個緩沖區標志 glGenRenderbuffers(1, &colorFrameBuffer) //將標識符綁定到GL_FRAMEBUFFER glBindFramebuffer(GLenum(GL_FRAMEBUFFER), colorFrameBuffer) //將渲染緩存區myColorRenderBuffer 通過glFramebufferRenderbuffer函數綁定到 GL_COLOR_ATTACHMENT0上。 glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), colorRederBuffer) }
6.開始正式繪制
func renderLayer() { //1.設置背景顏色 glClearColor(1, 0, 0, 1) //2.清楚深度緩沖區 glClear(GLbitfield(GL_DEPTH_BUFFER_BIT)) //3.設置視口 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)) //4.讀取頂點着色程序、片元着色程序 let spath = Bundle.main.path(forResource: "shaderv", ofType: "vsh") ?? "" let fpath = Bundle.main.path(forResource: "shaderf", ofType: "fsh") ?? "" let (sucess,program) = loadShader(vertexPath: spath, fragmentPath: fpath) if !sucess { return } //5.設置頂點、紋理坐標 let vertexs:[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, ] //6.處理定點數據(copy到緩沖區) var verbuffer = GLuint() glGenBuffers(1, &verbuffer) glBindBuffer(GLenum(GL_ARRAY_BUFFER), verbuffer) glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * 30, vertexs, GLenum(GL_DYNAMIC_DRAW)) //7.將頂點數據通過Programe傳遞到頂點着色程序的position屬性上 //1.glGetAttribLocation,用來獲取vertex attribute的入口的. //2.告訴OpenGL ES,通過glEnableVertexAttribArray,打開開關 //3.最后數據是通過glVertexAttribPointer傳遞過去的。 //頂點坐標 //(1)注意:第二參數字符串必須和shaderv.vsh中的輸入變量:position保持一致 let positon = glGetAttribLocation(program, "position") //(2).設置合適的格式從buffer里面讀取數據 glEnableVertexAttribArray(GLuint(positon)) //(3).設置讀取方式 //參數1:index,頂點數據的索引 //參數2:size,每個頂點屬性的組件數量,1,2,3,或者4.默認初始值是4. //參數3:type,數據中的每個組件的類型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默認初始值為GL_FLOAT //參數4:normalized,固定點數據值是否應該歸一化,或者直接轉換為固定值。(GL_FALSE) //參數5:stride,連續頂點屬性之間的偏移量,默認為0; //參數6:指定一個指針,指向數組中的第一個頂點屬性的第一個組件。默認為0 glVertexAttribPointer(GLuint(positon), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 5), UnsafeRawPointer(bitPattern: 0)) //8.紋理坐標數據通過Programe傳遞到頂點着色程序的textCoordinate屬性上 let texture = glGetAttribLocation(program, "textCoordinate") glEnableVertexAttribArray(GLuint(texture)) glVertexAttribPointer(GLuint(texture), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 5), UnsafeRawPointer(bitPattern:MemoryLayout<GLfloat>.size * 3)) //9.加載紋理圖片 setUpTextureImage(imageName: "timg.jpeg") //10.設置紋理采樣器 glUniform1i(glGetUniformLocation(program, "colorMap"), 0) //11.繪制 glDrawArrays(GLenum(GL_TRIANGLES), 0, 6) //12.提交 context.presentRenderbuffer(Int(GL_RENDERBUFFER)) } //設置紋理圖片 func setUpTextureImage(imageName:String) { guard let image = UIImage(named: imageName)?.cgImage else { return } let width = image.width let height = image.height //開辟內存,繪制到這個內存上去 let spriteData: UnsafeMutablePointer = UnsafeMutablePointer<GLubyte>.allocate(capacity: MemoryLayout<GLubyte>.size * width * height * 4) UIGraphicsBeginImageContext(CGSize(width: width, height: height)) //獲取context let spriteContext = CGContext(data: spriteData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: image.colorSpace!, bitmapInfo: image.bitmapInfo.rawValue) spriteContext?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height)) UIGraphicsEndImageContext() //綁定紋理 glBindTexture(GLenum(GL_TEXTURE_2D), 0) //設置紋理參數 //縮小/放大過濾器 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) //載入紋理 /* 參數1:紋理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D 參數2:加載的層次,一般設置為0 參數3:紋理的顏色值GL_RGBA 參數4:寬 參數5:高 參數6:border,邊界寬度 參數7:format 參數8:type 參數9:紋理數據 */ glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE),spriteData) //釋放內存 free(spriteData) }
7.稀構方法中清空緩沖區
deinit { glDeleteBuffers(1, &colorFrameBuffer) glDeleteBuffers(1, &colorRederBuffer) }
- 加載一張紋理圖片
func setUpTextureImage(imageName:String) { guard let image = UIImage(named: imageName)?.cgImage else { return } let width = image.width let height = image.height //開辟內存,繪制到這個內存上去 let spriteData: UnsafeMutablePointer = UnsafeMutablePointer<GLubyte>.allocate(capacity: MemoryLayout<GLubyte>.size * width * height * 4) UIGraphicsBeginImageContext(CGSize(width: width, height: height)) //獲取context let spriteContext = CGContext(data: spriteData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: image.colorSpace!, bitmapInfo: image.bitmapInfo.rawValue)
//圖片反轉,
spriteContext?.translateBy(x: 0, y: CGFloat(height))//向下平移圖片的高度
spriteContext?.scaleBy(x: 1, y: -1)//反轉圖片
spriteContext?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height)) UIGraphicsEndImageContext() //綁定紋理 glBindTexture(GLenum(GL_TEXTURE_2D), 0) //設置紋理參數 //縮小/放大過濾器 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) //載入紋理 /* 參數1:紋理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D 參數2:加載的層次,一般設置為0 參數3:紋理的顏色值GL_RGBA 參數4:寬 參數5:高 參數6:border,邊界寬度 參數7:format 參數8:type 參數9:紋理數據 */ glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE),spriteData) //釋放內存 free(spriteData) }
- 加載着色器程序方法
func loadShader(vertexPath:String,fragmentPath:String) -> (Bool,GLuint){ let program:GLuint = glCreateProgram() //vertexShader guard let verShader:GLuint = compileShader(type: GLenum(GL_VERTEX_SHADER), filePath: vertexPath) else { return (false,program) } //把編譯后的着色器代碼附着到最終的程序上 glAttachShader(program, verShader) //釋放不需要的shader glDeleteShader(verShader) //fragmentShader guard let fragShader = compileShader(type: GLenum(GL_FRAGMENT_SHADER), filePath: fragmentPath)else{ return (false,program) } glAttachShader(program, fragShader) glDeleteShader(fragShader) //鏈接着色器代程序 glLinkProgram(program) //獲取鏈接狀態 var status:GLint = 0 glGetProgramiv(program, GLenum(GL_LINK_STATUS), &status) if status == GLenum(GL_FALSE){ print("link Error") //打印錯誤信息 let message = UnsafeMutablePointer<GLchar>.allocate(capacity: 512) glGetProgramInfoLog(program, GLsizei(MemoryLayout<GLchar>.size * 512), nil, message) let str = String.init(utf8String: message) print(str ?? "沒有取到ProgramInfoLog") return (false,program) }else{ print("link sucess!") //鏈接成功,使用着色器程序 glUseProgram(program) return (true,program) } }
//讀取並編譯着色器程序
func compileShader(type:GLenum,filePath:String) -> GLuint? {
//創建一個空着色器
let verShader:GLuint = glCreateShader(type) //獲取源文件中的代碼字符串 guard let shaderString = try? String.init(contentsOfFile: filePath, encoding: String.Encoding.utf8)else { return nil } //轉成C字符串賦值給已創建的shader shaderString.withCString { (pointer) in var pon:UnsafePointer<GLchar>? = pointer glShaderSource(verShader, 1, &pon, nil) } //編譯 glCompileShader(verShader) return verShader }
- 頂點着色器代碼
attribute vec4 position; attribute vec2 textCoordinate; varying lowp vec2 varyTextCoord; uniform mat4 rotateMatrix; void main() { // varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y); varyTextCoord = textCoordinate; gl_Position = position; // gl_Position = position * rotateMatrix; }
- 片元着色器代碼
varying lowp vec2 varyTextCoord; uniform sampler2D colorMap; void main() { gl_FragColor = texture2D(colorMap, varyTextCoord); // gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0 - varyTextCoord.y)); }
- 關於紋理反轉問題
由於iOSlayer中坐標系OpneGL坐標系不一致(頂點在左上角)所以加載出來的圖片會到置,需要做一下處理使圖片正,主要紋理反轉的方法有如下:
//1.矩陣反轉
func rotateTextureImage(program:GLuint) { //獲取旋轉180度的矩陣 var rotateM = GLKMatrix4MakeZRotation(Float.pi).getArray() //rotateM = GLKMatrix4Identity.getArray() glUniformMatrix4fv(glGetUniformLocation(program, "rotateMatrix"), 1, 0, rotateM) } extension GLKMatrix4 { /// 轉成數組 /// - Returns: 結果數組 func getArray() ->[Float] { [ m00,m01,m02,m03, m10,m11,m12,m13, m20,m21,m22,m23, m30,m31,m32,m33, ] } }
//2.加載圖片時反轉
spriteContext?.translateBy(x: 0, y: CGFloat(height))//向下平移圖片的高度 spriteContext?.scaleBy(x: 1, y: -1)//反轉圖片
//3.坐標反轉,改變紋理坐標
let vertexs:[GLfloat] = [ 0.5, -0.5, -1.0, 1.0f, 1.0f, -0.5, 0.5, -1.0, 0.0f, 0.0f, -0.5, -0.5, -1.0, 0.0f, 1.0f, 0.5, 0.5, -1.0, 1.0f, 0.0f, -0.5, 0.5, -1.0, 0.0f, 0.0f, 0.5, -0.5, -1.0, 1.0f, 1.0f, ]
//4.頂點着色器中反轉
//頂點着色器傳遞給片元着色器是改變Y坐標1-y坐標,使Y方向反轉 varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y);
//5.片元着色器中反轉
//直接在片元着色器中反轉Y坐標 gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0 - varyTextCoord.y));
因為想着在着色器程序中使用較少的代碼原則,是傾向於使用第2種方法
- 具體github代碼地址:
https://github.com/duzhaoquan/OpenGLESLoadImage.git