前言
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)天空盒的效果,通過較為復雜的方式,去更好學習天空盒的原理。
通過對頂點、紋理、變換矩陣的處理,能更好掌握圖形學中三維空間的理解。
具體的代碼在這里。