Metal入門教程(七)天空盒全景

前言

Metal入門教程(一)圖片繪制
Metal入門教程(二)三維變換
Metal入門教程(三)攝像頭采集渲染
Metal入門教程(四)灰度計算
Metal入門教程(五)視頻渲染
Metal入門教程(六)邊界檢測

前面的教程介紹了Metal的圖片繪制、三維變換、視頻渲染、用MetalPerformanceShaders處理數(shù)據(jù)以及用計算管道實現(xiàn)灰度計算和sobel邊界檢測,這次對Metal的三維變換做更復雜的嘗試——天空盒。

Metal系列教程的代碼地址;
OpenGL ES系列教程在這里;

你的star和fork是我的源動力,你的意見能讓我走得更遠。

正文

核心思路

天空盒的原理:想象有一個正方體,正方體的六個面都貼著紋理;攝像機在正方體的中心,近平面在正方體內部,遠平面在正方體外面,隨著攝像機的旋轉可以看到整個正方體的貼圖。
基于此,我們可以初步確定實現(xiàn)的思路:
1、在三維空間繪制一個正方體;
2、給正方體六個面進行貼圖;
3、把攝像機放在正方體中心;
4、隨著時間改變攝像機的位置;

接下來我們考慮兩個問題:
六個面共十二個三角形,在繪制過程中是否會重疊以及是否需要使用深度測試?
按照我們的思路,十二個三角形中,每個三角形最多與另外一個三角形重疊(試想一條線穿過正方體,除了頂點外最多只能接觸兩個面)。
基于上面的分析,因為在正方體的中心,近平面在內部而遠平面在外面,重疊的兩個三角形必然一個在平截體的內部,一個在平截體的外部。故而這里不使用深度測試。

具體步驟

1、繪制一個正方體

首先,我們定義8個頂點。

        // 頂點坐標,                      頂點顏色,                  紋理坐標,
        // 正方體上面的四個點
        {{-0.5f, 0.5f, 0.5f, 1.0f},      {1.0f, 0.0f, 0.0f},       {0.0f, 1.0f}},//左上 0
        {{0.5f, 0.5f, 0.5f, 1.0f},       {0.0f, 1.0f, 0.0f},       {1.0f, 1.0f}},//右上 1
        {{-0.5f, -0.5f, 0.5f, 1.0f},     {0.0f, 0.0f, 1.0f},       {0.0f, 0.0f}},//左下 2
        {{0.5f, -0.5f, 0.5f, 1.0f},      {1.0f, 1.0f, 1.0f},       {1.0f, 0.0f}},//右下 3
        
        // 正方體下面的四個點
        {{-0.5f, 0.5f, -0.5f, 1.0f},      {1.0f, 0.0f, 0.0f},       {0.0f, 1.0f}},//左上 4
        {{0.5f, 0.5f, -0.5f, 1.0f},       {0.0f, 1.0f, 0.0f},       {1.0f, 1.0f}},//右上 5
        {{-0.5f, -0.5f, -0.5f, 1.0f},     {0.0f, 0.0f, 1.0f},       {0.0f, 0.0f}},//左下 6
        {{0.5f, -0.5f, -0.5f, 1.0f},      {1.0f, 1.0f, 1.0f},       {1.0f, 0.0f}},//右下 7
2、頂點與紋理位置對應

假設把下圖的拼成一個正方體,根據(jù)我們定義的0~7號節(jié)點,可以一一標志出對應的頂點所在,如下:


頂點標注圖
3、紋理轉換

上面的頂點標注圖在加載、處理的過程中并不方便,故而需要把圖片預處理成width=x, height=6*x的大小。


天空盒紋理圖

根據(jù)前面兩個圖,我們可以推導出最終天空盒的頂點數(shù)據(jù)如下:

        // 頂點坐標,                      頂點顏色,                  紋理坐標,

        // 上面
        {{-6.0f, 6.0f, 6.0f, 1.0f},      {1.0f, 0.0f, 0.0f},       {0.0f, 2.0f/6}},//左上 0
        {{-6.0f, -6.0f, 6.0f, 1.0f},     {0.0f, 0.0f, 1.0f},       {0.0f, 3.0f/6}},//左下 2
        {{6.0f, -6.0f, 6.0f, 1.0f},      {1.0f, 1.0f, 1.0f},       {1.0f, 3.0f/6}},//右下 3

        {{-6.0f, 6.0f, 6.0f, 1.0f},      {1.0f, 0.0f, 0.0f},       {0.0f, 2.0f/6}},//左上 0
        {{6.0f, 6.0f, 6.0f, 1.0f},       {0.0f, 1.0f, 0.0f},       {1.0f, 2.0f/6}},//右上 1
        {{6.0f, -6.0f, 6.0f, 1.0f},      {1.0f, 1.0f, 1.0f},       {1.0f, 3.0f/6}},//右下 3


        // 下面
        {{-6.0f, 6.0f, -6.0f, 1.0f},     {1.0f, 0.0f, 0.0f},       {0.0f, 4.0f/6}},//左上 4
        {{6.0f, 6.0f, -6.0f, 1.0f},      {0.0f, 1.0f, 0.0f},       {1.0f, 4.0f/6}},//右上 5
        {{6.0f, -6.0f, -6.0f, 1.0f},     {1.0f, 1.0f, 1.0f},       {1.0f, 3.0f/6}},//右下 7

        {{-6.0f, 6.0f, -6.0f, 1.0f},     {1.0f, 0.0f, 0.0f},       {0.0f, 4.0f/6}},//左上 4
        {{-6.0f, -6.0f, -6.0f, 1.0f},    {0.0f, 0.0f, 1.0f},       {0.0f, 3.0f/6}},//左下 6
        {{6.0f, -6.0f, -6.0f, 1.0f},     {1.0f, 1.0f, 1.0f},       {1.0f, 3.0f/6}},//右下 7
        
        // 左面
        {{-6.0f, 6.0f, 6.0f, 1.0f},      {1.0f, 0.0f, 0.0f},       {0.0f, 1.0f/6}},//左上 0
        {{-6.0f, -6.0f, 6.0f, 1.0f},     {0.0f, 0.0f, 1.0f},       {1.0f, 1.0f/6}},//左下 2
        {{-6.0f, 6.0f, -6.0f, 1.0f},     {1.0f, 0.0f, 0.0f},       {0.0f, 2.0f/6}},//左上 4

        {{-6.0f, -6.0f, 6.0f, 1.0f},     {0.0f, 0.0f, 1.0f},       {1.0f, 1.0f/6}},//左下 2
        {{-6.0f, 6.0f, -6.0f, 1.0f},     {1.0f, 0.0f, 0.0f},       {0.0f, 2.0f/6}},//左上 4
        {{-6.0f, -6.0f, -6.0f, 1.0f},    {0.0f, 0.0f, 1.0f},       {1.0f, 2.0f/6}},//左下 6


        // 右面
        {{6.0f, 6.0f, 6.0f, 1.0f},       {0.0f, 1.0f, 0.0f},       {1.0f, 0.0f/6}},//右上 1
        {{6.0f, -6.0f, 6.0f, 1.0f},      {1.0f, 1.0f, 1.0f},       {0.0f, 0.0f/6}},//右下 3
        {{6.0f, 6.0f, -6.0f, 1.0f},      {0.0f, 1.0f, 0.0f},       {1.0f, 1.0f/6}},//右上 5

        {{6.0f, -6.0f, 6.0f, 1.0f},      {1.0f, 1.0f, 1.0f},       {0.0f, 0.0f/6}},//右下 3
        {{6.0f, 6.0f, -6.0f, 1.0f},      {0.0f, 1.0f, 0.0f},       {1.0f, 1.0f/6}},//右上 5
        {{6.0f, -6.0f, -6.0f, 1.0f},     {1.0f, 1.0f, 1.0f},       {0.0f, 1.0f/6}},//右下 7
        
        // 前面
        {{-6.0f, -6.0f, 6.0f, 1.0f},     {0.0f, 0.0f, 1.0f},       {0.0f, 4.0f/6}},//左下 2
        {{6.0f, -6.0f, 6.0f, 1.0f},      {1.0f, 1.0f, 1.0f},       {1.0f, 4.0f/6}},//右下 3
        {{6.0f, -6.0f, -6.0f, 1.0f},     {1.0f, 1.0f, 1.0f},       {1.0f, 5.0f/6}},//右下 7

        {{-6.0f, -6.0f, 6.0f, 1.0f},     {0.0f, 0.0f, 1.0f},       {0.0f, 4.0f/6}},//左下 2
        {{-6.0f, -6.0f, -6.0f, 1.0f},    {0.0f, 0.0f, 1.0f},       {0.0f, 5.0f/6}},//左下 6
        {{6.0f, -6.0f, -6.0f, 1.0f},     {1.0f, 1.0f, 1.0f},       {1.0f, 5.0f/6}},//右下 7

        // 后面
        {{-6.0f, 6.0f, 6.0f, 1.0f},      {1.0f, 0.0f, 0.0f},       {1.0f, 5.0f/6}},//左上 0
        {{6.0f, 6.0f, 6.0f, 1.0f},       {0.0f, 1.0f, 0.0f},       {0.0f, 5.0f/6}},//右上 1
        {{6.0f, 6.0f, -6.0f, 1.0f},      {0.0f, 1.0f, 0.0f},       {0.0f, 6.0f/6}},//右上 5

        {{-6.0f, 6.0f, 6.0f, 1.0f},      {1.0f, 0.0f, 0.0f},       {1.0f, 5.0f/6}},//左上 0
        {{-6.0f, 6.0f, -6.0f, 1.0f},     {1.0f, 0.0f, 0.0f},       {1.0f, 6.0f/6}},//左上 4
        {{6.0f, 6.0f, -6.0f, 1.0f},      {0.0f, 1.0f, 0.0f},       {0.0f, 6.0f/6}},//右上 5
        

有了以上的頂點數(shù)據(jù)和紋理數(shù)據(jù),我們可以接著

4、調整投影矩陣和模型變換矩陣

這里我們用GLKMatrix4MakeLookAt來生成模型變換矩陣

    // 調整眼睛的位置
    self.eyePosition = GLKVector3Make(2.0f * sinf(angle),
                                      2.0f * cosf(angle),
                                      0.0f);
    
    // 調整觀察的位置
    self.lookAtPosition = GLKVector3Make(2.0f * sinf(angleLook),
                                         2.0f * cosf(angleLook),
                                         2.0f);

    GLKMatrix4 modelViewMatrix = GLKMatrix4MakeLookAt(
                                                      self.eyePosition.x,
                                                      self.eyePosition.y,
                                                      self.eyePosition.z,
                                                      self.lookAtPosition.x,
                                                      self.lookAtPosition.y,
                                                      self.lookAtPosition.z,
                                                      self.upVector.x,
                                                      self.upVector.y,
                                                      self.upVector.z); // 模型變換矩陣

這里的眼睛位置就是平截體起點,觀察方向是指眼睛到遠平面中心點的方向,如下:


投影矩陣如下,對應的參數(shù)是上面的視野角、寬高比、近平面距離、遠平面距離。
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(85.0f), aspect, 0.1f, 20.f); // 投影變換矩陣

5、shader繪制
vertex RasterizerData
vertexShader(uint vertexID [[ vertex_id ]], // 頂點索引
             constant LYVertex *vertexArray [[ buffer(LYVertexInputIndexVertices) ]], // 頂點數(shù)據(jù)
             constant LYMatrix *matrix [[ buffer(LYVertexInputIndexMatrix) ]]) { // 變換矩陣
    RasterizerData out; // 輸出數(shù)據(jù)
    out.clipSpacePosition = matrix->projectionMatrix * matrix->modelViewMatrix * vertexArray[vertexID].position; // 變換處理
    out.textureCoordinate = vertexArray[vertexID].textureCoordinate; // 紋理坐標
    out.pixelColor = vertexArray[vertexID].color; // 頂點顏色,調試用
    return out;
}

fragment float4
samplingShader(RasterizerData input [[stage_in]],
               texture2d<half> textureColor [[ texture(LYFragmentInputIndexTexture) ]])
{
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear); // 采樣器
    half4 colorTex = textureColor.sample(textureSampler, input.textureCoordinate); // 紋理顏色
//    half4 colorTex = half4(input.pixelColor.x, input.pixelColor.y, input.pixelColor.z, 1); // 頂點顏色,方便調試
    return float4(colorTex);
}

頂點shader是正常對頂點進行變換處理,紋理坐標、頂點顏色讀取buffer的值;
片元shader是從紋理中讀取顏色,為了方便調試,可以注釋上面的紋理顏色,采用下面的頂點顏色可以快速定位紋理坐標、頂點坐標的問題。

注意事項

在繪制正方體的時候,可以把正方體縮小,整個放在平截體內,這樣可以看到完整的正方體,便于調整頂點坐標和紋理坐標。
此時需要解決重復渲染的問題,常用兩種辦法:

  • 方案1、圖元朝向做剔除;
        [renderEncoder setFrontFacingWinding:MTLWindingCounterClockwise];
        [renderEncoder setCullMode:MTLCullModeBack];
  • 方案2、深度測試剔除;
    // 創(chuàng)建深度緩存
    MTLDepthStencilDescriptor *depthStencilDescriptor = [MTLDepthStencilDescriptor new];
    depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionLess;
    self.depthStencilState = [self.mtkView.device newDepthStencilStateWithDescriptor:depthStencilDescriptor];

    // 然后設置深度測試
    [renderEncoder setDepthStencilState:self.depthStencilState];

實現(xiàn)過程還有另外的一個問題,棱角效果太明顯。這個是因為天空盒太小,能投影到近平面的面積過小,導致棱角分明。解決方案是把天空盒的邊長適當放大(不要超過遠平面),使得天空盒更多區(qū)域能投影到屏幕,減少棱角區(qū)域的面積。

附錄 ---- 天空盒的另一種簡單實現(xiàn)

注意看前文步驟,shader讀取紋理用的是texture2d格式,而天空盒還有另外一種方案是通過立方體紋理textureCube讀取。
由于篇幅,不再贅述具體步驟,詳見demo--TextureCube。
需要注意的是:
1、紋理加載方案不同,要用-textureCubeDescriptorWithPixelFormat方法,同時紋理上傳接口也不相同。如下:

    MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor textureCubeDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm size:image.size.width mipmapped:NO];
    self.texture = [self.mtkView.device newTextureWithDescriptor:textureDescriptor];
    
    Byte *imageBytes = [self loadImage:image];
    NSInteger pixels = image.size.width * image.size.width;
    if (imageBytes) {
        for (int i = 0; i < 6; i++)
        {
            [self.texture replaceRegion:MTLRegionMake2D(0, 0, image.size.width, image.size.width)
                            mipmapLevel:0
                                  slice:i
                              withBytes:imageBytes + (i * pixels * 4)
                            bytesPerRow:4 * (NSInteger)image.size.width
                          bytesPerImage:pixels * 4];
        }
        
        free(imageBytes);
        imageBytes = NULL;
    }

2、shader中的紋理坐標不同,這里的紋理坐標使用的是頂點坐標,而之前的方案使用的是頂點的紋理坐標。

out.textureCoordinate = vertexArray[vertexID].position.xyz;

注意,這里使用的是頂點變換前的坐標,如果使用頂點變換后的坐標,會導致的現(xiàn)象是視角無法旋轉。

// 試試代碼改為下面這段
out.textureCoordinate = out.clipSpacePosition.xyz;

總結

demo嘗試實現(xiàn)天空盒的效果,通過較為復雜的方式,去更好學習天空盒的原理。
通過對頂點、紋理、變換矩陣的處理,能更好掌握圖形學中三維空間的理解。
具體的代碼在這里。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容