iOS視覺-- (05) OpenGL ES+GLSL實現正方體貼6張圖解析
上一篇文章我們通過金字塔延伸到了正方體,然後到這篇正方體每一個面貼一張圖。 先看效果圖:Demo
接下來讓我們開始學習OpenGL 一個重要‼️的知識點:紋理 借鑑部落格:半紙淵--基礎紋理 前言:之前我們說過紋理可以簡單理解為圖片,但是紋理不簡簡單單🟰圖片。 * #### 1. Texture 是什麼?
Texture 紋理,就是一堆被精心排列過的畫素; Texture 在 OpenGL 裡面有很多種類,但在 ES 版本中就兩種:Texture_2D 、 Texture_CubeMap
-
Texture_2D:
就是 {x, y} 二維空間下的畫素呈現,也就是說,由效果圖上可知,很難做到使正方體的六個面出現不同的畫素組合;圖片處理一般都使用這個模式;[x 、y 屬於 [0, 1] 這個範圍]
-
Texture_CubeMap:
就是 { x, y, z } 三維空間下的畫素呈現,也就如效果圖中演示的正方體的六個面可以出現不同的畫素組合;它一般是用於做環境貼圖——就是製作一個環境,讓 3D 模型如同置身於真實環境中【卡通環境中也行】。[x、y、z 屬於 [-1, 1] 這個範圍,就是與 Vertex Position 的值範圍一致]
注:上面提到的所有座標範圍是指有效渲染範圍,也就是說你如果提供的紋理座標超出了這個範圍也沒有問題,只不過超出的部分就不渲染了;
頂點資料表示如下:
* Texture_2D:
//------------- 正方體 -------------
let attrArr: [GLfloat] = [
// 頂點:(x, y, z) 顏色:(r, g, b) 紋理: (s, t)
// 前面
-0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0, 0.0, // 前左上 0
-0.5, -0.5, 0.5, 1.0, 1.0, 1.0, 0.0, 1.0, // 前左下 1
0.5, -0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, // 前右下 2
0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 0.0, // 前右上 3
...
]
* Texture_CubeMap:
let attrArr: [GLfloat] = [
// 頂點:(x, y, z) 顏色:(r, g, b) 紋理: (s, t, p)
// 前面
-1.0, 1.0, 1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 1.0, // 前左上 0
-1.0, -1.0, 1.0, 0.0, 1.0, 0.0, -1.0, -1.0, 1.0, // 前左下 1
1.0, -1.0, 1.0, 0.0, 0.0, 1.0, 1.0, -1.0, 1.0, // 前右下 2
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, // 前右上 3
...
]
⚠️ps: CubeMap 裡面的紋理座標和頂點資料是一樣的
* 載入CubeMap紋理
CubeMap共有6個面,然後分別設定 GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x8515 GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516 GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x8517 GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518 GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x8519 GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A
程式碼如下:注意⚠️:這裡是cubeMap 6張圖片的寬高要一致 ``` //7.1 設定立方體紋理 func setupCubeTexture() { //7.繫結紋理到預設的紋理ID(這裡只有一張圖片,故而相當於預設於片元著色器裡面的us2d_texture) glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
for i in 0..<6 {
let spriteImage: CGImage = UIImage(named: "timg-\(i+1)")!.cgImage!
//2.讀取圖片的大小:寬和高 注意⚠️:這裡是cubeMap 6張圖片的寬高要一致
let width = 512//spriteImage.width
let height = 512//spriteImage.height
//3.獲取圖片位元組數: 寬x高x4(RGBA)
// let spriteData: UnsafeMutablePointer = UnsafeMutablePointer<GLbyte>.allocate(capacity: MemoryLayout<GLbyte>.size * width * height * 4)
let spriteData: UnsafeMutableRawPointer = calloc(width * height * 4, MemoryLayout<GLbyte>.size)
//4.建立上下文
/*
引數1:data,指向要渲染的繪製圖像的記憶體地址
引數2:width,bitmap的寬度,單位為畫素
引數3:height,bitmap的高度,單位為畫素
引數4:bitPerComponent,記憶體中畫素的每個元件的位數,比如32位RGBA,就設定為8
引數5:bytesPerRow,bitmap的每一行的記憶體所佔的位元數
引數6:colorSpace,bitmap上使用的顏色空間 kCGImageAlphaPremultipliedLast:RGBA
let colorSpace = CGColorSpaceCreateDeviceRGB()
*/
let spriteContext: CGContext = CGContext(data: spriteData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: spriteImage.colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
//5.在CGContextRef上繪圖
let rect = CGRect(x: 0, y: 0, width: width, height: height)
spriteContext.draw(spriteImage, in: rect)
//載入紋理2D資料 就是載入紋理畫素到 GPU 的方法
/*
引數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_CUBE_MAP_POSITIVE_X + Int32(i)), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), spriteData)
//釋放spriteData
free(spriteData)
}
//設定紋理屬性
/*
引數1:紋理維度
引數2:線性過濾、為s,t座標設定模式
引數3:wrapMode,環繞模式
*/
glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
//繫結紋理
/*
引數1:紋理維度
引數2:紋理ID,因為只有一個紋理,給0就可以了。
*/
glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
}
```
因為我們的設定的紋理座標由兩位:[s, t] --> [s, t, p],所以著色器中的紋理座標要做相應的改變,還有渲染那裡讀取資料的時候也做相應改變。 * 頂點著色器程式碼: ``` //紋理座標vec2 --> vec3 attribute vec4 position; attribute vec4 positionColor; //頂點顏色 attribute vec3 textCoordinate; //紋理座標 uniform mat4 projectionMatrix; //投影矩陣 uniform mat4 modelViewMatrix; //模型檢視矩陣
varying lowp vec4 varyColor; //頂點顏色 varying lowp vec3 varyTextCoord; //傳遞給片元著色器紋理座標
void main() { varyColor = positionColor; varyTextCoord = textCoordinate;
vec4 vPos;
vPos = projectionMatrix * modelViewMatrix * position;
gl_Position = vPos;
}
```
- 片元著色器程式碼: ``` //紋理座標vec2 --> vec3 varying lowp vec4 varyColor; //頂點顏色 varying lowp vec3 varyTextCoord; //頂點著色器傳遞過來的紋理座標
//uniform sampler2D colorMap; //紋理 uniform samplerCube us2d_texture;
void main() { gl_FragColor = textureCube(us2d_texture, varyTextCoord) * varyColor; }
```
渲染程式碼:
步長和紋理座標做相應的調整即可,就不貼了
到此正方體貼圖工作就完成了。詳細請檢視原始碼
但是從借鑑的部落格半紙淵--基礎紋理,看到他能實現下影象魔方的效果,既然都做到多面貼圖了,所以也想試試看。
通過檢視他的原始碼,這種實現方式也是:glTexImage2D,只不過最後一個引數資料是個顏色陣列 * 載入紋理的程式碼就變成這樣: ``` //7.2 設定立方體畫素紋理 func setupCubePixelsTexture() { //7.繫結紋理到預設的紋理ID(這裡只有一張圖片,故而相當於預設於片元著色器裡面的us2d_texture) glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
for i in 0..<6 {
glTexImage2D(GLenum(GL_TEXTURE_CUBE_MAP_POSITIVE_X + Int32(i)), 0, GL_RGBA, 2, 2, 0, GLenum(GL_RGBA), GLenum(GL_FLOAT), texCubemapPixelDatas[i])
}
//設定紋理屬性
/*
引數1:紋理維度
引數2:線性過濾、為s,t座標設定模式
引數3:wrapMode,環繞模式
*/
glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
//繫結紋理
/*
引數1:紋理維度
引數2:紋理ID,因為只有一個紋理,給0就可以了。
*/
glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
}
```
這並不是我們想要的效果,而且連方格都沒有顯示出來,雖然中間看似有分割線。但是離效果圖還是天差地別的。怎麼回事呢?對比了一下發現在過濾方式不同:GL_LINEAR 和 GL_NEAREST
摘抄自:mChenys -- 六、OpenGL ES紋理的使用 當紋理大小要被擴大或者縮小的時候,我們需要使用紋理過濾明確說明會發生什麼,當我們在渲染表面上繪製一個紋理時,那個紋理的紋理元素可能無法精確地對映到OpenGL生成的片段上,有2種情況:縮小或者放大。 當我們盡力把幾個紋理元素擠進一個片段時,縮小就會發生了,當把一個紋理元素擴充套件到許多片段時,放大就發生了。 針對每一種情況,我們都可以配置OpenGL使用一個紋理過濾器,我會使用下面的影象闡述每一種過濾模式:
* GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL預設的紋理過濾方式。當設定為GL_NEAREST的時候,OpenGL會選擇中心點最接近紋理座標的那個畫素。下圖中你可以看到四個畫素,加號代表紋理座標。左上角那個紋理畫素的中心距離紋理座標最近,所以它會被選擇為樣本顏色:
這種方式為每個片段選擇最近的紋理元素,當放大紋理時它的鋸齒效果看起來相當明顯,每個紋理單元都清楚地顯示為一個小方塊。
當我們縮小紋理時,因為沒有足夠的片段來繪製所有的紋理單元,許多細節將會丟失。
* GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基於紋理座標附近的紋理畫素,計算出一個插值,近似出這些紋理畫素之間的顏色。一個紋理畫素的中心距離紋理座標越近,那麼這個紋理畫素的顏色對最終的樣本顏色的貢獻越大。下圖中你可以看到返回的顏色是鄰近畫素的混合色:
線性過濾使用雙線插值平滑畫素之間的過渡,而不是每個片段使用最近的紋理元素,OpenGL會使用四個鄰接的紋理元素,並在他們之間用一個線性差值演算法做差值,這個演算法與前面介紹平滑著色的演算法一樣,之所以叫它雙線性,是因為它是沿兩個維度差值的,它會比近鄰過濾要平滑很多,但還是會有一些鋸齒顯示出來,因為我們把這個紋理擴充套件得太多了,但是鋸齒沒有最近鄰過濾那麼明顯。
PS:紋理放大時使用線性過濾(GL_LINEAR),縮小時用鄰近過濾(GL_NEAREST)
然後我們修改過濾方式為:鄰近過濾(GL_NEAREST)
效果如下:
這裡看到已經差不多了和他的一樣了。但是總覺得怪怪的。就是每一個面都會有一塊是黑色的,而對照的卻不是這樣的。然後再去仔細看了看。後面發現原來還是在這個方法上出現了問題:
glTexImage2D(GLenum(GL_TEXTURE_CUBE_MAP_POSITIVE_X + Int32(i)), 0, GL_RGBA, 2, 2, 0, GLenum(GL_RGBA), GLenum(GL_FLOAT), texCubemapPixelDatas[i]) //載入紋理2D資料 就是載入紋理畫素到 GPU 的方法 引數1:紋理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D 引數2:載入的層次,一般設定為0 引數3:紋理的顏色值GL_RGBA 引數4:寬 引數5:高 引數6:border,邊界寬度 引數7:format 引數8:type 引數9:紋理資料 這裡的(引數3:紋理的顏色值,引數7:format)我們傳的是GL_RGBA,而數組裡面只有(r, g, b)並沒有a,所以出現取值有問題吧。改成 GL_RGB
執行結果: