iOS視覺-- (05) OpenGL ES+GLSL實現正方體貼6張圖解析

語言: CN / TW / HK

上一篇文章我們通過金字塔延伸到了正方體,然後到這篇正方體每一個面貼一張圖。 先看效果圖:Demo

效果圖

接下來讓我們開始學習OpenGL 一個重要‼️的知識點:紋理 借鑑部落格:半紙淵--基礎紋理 前言:之前我們說過紋理可以簡單理解為圖片,但是紋理不簡簡單單🟰圖片。 * #### 1. Texture 是什麼?

Texture 紋理,就是一堆被精心排列過的畫素; Texture 在 OpenGL 裡面有很多種類,但在 ES 版本中就兩種:Texture_2D 、 Texture_CubeMap

  • Texture_2D:

    就是 {x, y} 二維空間下的畫素呈現,也就是說,由效果圖上可知,很難做到使正方體的六個面出現不同的畫素組合;圖片處理一般都使用這個模式;[x 、y 屬於 [0, 1] 這個範圍] 2D紋理座標

  • Texture_CubeMap:

    就是 { x, y, z } 三維空間下的畫素呈現,也就如效果圖中演示的正方體的六個面可以出現不同的畫素組合;它一般是用於做環境貼圖——就是製作一個環境,讓 3D 模型如同置身於真實環境中【卡通環境中也行】。[x、y、z 屬於 [-1, 1] 這個範圍,就是與 Vertex Position 的值範圍一致] 3D紋理座標

    注:上面提到的所有座標範圍是指有效渲染範圍,也就是說你如果提供的紋理座標超出了這個範圍也沒有問題,只不過超出的部分就不渲染了;

頂點資料表示如下: * 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使用一個紋理過濾器,我會使用下面的影象闡述每一種過濾模式:

圖1 * GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL預設的紋理過濾方式。當設定為GL_NEAREST的時候,OpenGL會選擇中心點最接近紋理座標的那個畫素。下圖中你可以看到四個畫素,加號代表紋理座標。左上角那個紋理畫素的中心距離紋理座標最近,所以它會被選擇為樣本顏色:

圖2

這種方式為每個片段選擇最近的紋理元素,當放大紋理時它的鋸齒效果看起來相當明顯,每個紋理單元都清楚地顯示為一個小方塊。

圖3

當我們縮小紋理時,因為沒有足夠的片段來繪製所有的紋理單元,許多細節將會丟失。

圖4 * GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基於紋理座標附近的紋理畫素,計算出一個插值,近似出這些紋理畫素之間的顏色。一個紋理畫素的中心距離紋理座標越近,那麼這個紋理畫素的顏色對最終的樣本顏色的貢獻越大。下圖中你可以看到返回的顏色是鄰近畫素的混合色:

圖5

線性過濾使用雙線插值平滑畫素之間的過渡,而不是每個片段使用最近的紋理元素,OpenGL會使用四個鄰接的紋理元素,並在他們之間用一個線性差值演算法做差值,這個演算法與前面介紹平滑著色的演算法一樣,之所以叫它雙線性,是因為它是沿兩個維度差值的,它會比近鄰過濾要平滑很多,但還是會有一些鋸齒顯示出來,因為我們把這個紋理擴充套件得太多了,但是鋸齒沒有最近鄰過濾那麼明顯。

圖6

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

執行結果:

執行結果