前言
Metal入門(mén)教程(一)圖片繪制
Metal入門(mén)教程(二)三維變換
Metal入門(mén)教程(三)攝像頭采集渲染
Metal入門(mén)教程(四)灰度計(jì)算
Metal入門(mén)教程(五)視頻渲染
Metal入門(mén)教程(六)邊界檢測(cè)
前面的教程介紹了Metal的圖片繪制疙描、三維變換、視頻渲染嚼贡、用MetalPerformanceShaders處理數(shù)據(jù)以及用計(jì)算管道實(shí)現(xiàn)灰度計(jì)算和sobel邊界檢測(cè)违柏,這次對(duì)Metal的三維變換做更復(fù)雜的嘗試——天空盒毁兆。
Metal系列教程的代碼地址;
OpenGL ES系列教程在這里;
你的star和fork是我的源動(dòng)力涮因,你的意見(jiàn)能讓我走得更遠(yuǎn)。
正文
核心思路
天空盒的原理:想象有一個(gè)正方體坷虑,正方體的六個(gè)面都貼著紋理甲馋;攝像機(jī)在正方體的中心,近平面在正方體內(nèi)部迄损,遠(yuǎn)平面在正方體外面定躏,隨著攝像機(jī)的旋轉(zhuǎn)可以看到整個(gè)正方體的貼圖。
基于此芹敌,我們可以初步確定實(shí)現(xiàn)的思路:
1痊远、在三維空間繪制一個(gè)正方體;
2氏捞、給正方體六個(gè)面進(jìn)行貼圖碧聪;
3、把攝像機(jī)放在正方體中心液茎;
4逞姿、隨著時(shí)間改變攝像機(jī)的位置;
接下來(lái)我們考慮兩個(gè)問(wèn)題:
六個(gè)面共十二個(gè)三角形捆等,在繪制過(guò)程中是否會(huì)重疊以及是否需要使用深度測(cè)試滞造?
按照我們的思路,十二個(gè)三角形中楚里,每個(gè)三角形最多與另外一個(gè)三角形重疊(試想一條線穿過(guò)正方體断部,除了頂點(diǎn)外最多只能接觸兩個(gè)面)。
基于上面的分析班缎,因?yàn)樵谡襟w的中心蝴光,近平面在內(nèi)部而遠(yuǎn)平面在外面,重疊的兩個(gè)三角形必然一個(gè)在平截體的內(nèi)部达址,一個(gè)在平截體的外部蔑祟。故而這里不使用深度測(cè)試。
具體步驟
1沉唠、繪制一個(gè)正方體
首先疆虚,我們定義8個(gè)頂點(diǎn)。
// 頂點(diǎn)坐標(biāo)满葛, 頂點(diǎn)顏色径簿, 紋理坐標(biāo),
// 正方體上面的四個(gè)點(diǎn)
{{-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
// 正方體下面的四個(gè)點(diǎn)
{{-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嘀韧、頂點(diǎn)與紋理位置對(duì)應(yīng)
假設(shè)把下圖的拼成一個(gè)正方體篇亭,根據(jù)我們定義的0~7號(hào)節(jié)點(diǎn),可以一一標(biāo)志出對(duì)應(yīng)的頂點(diǎn)所在锄贷,如下:
3译蒂、紋理轉(zhuǎn)換
上面的頂點(diǎn)標(biāo)注圖在加載曼月、處理的過(guò)程中并不方便,故而需要把圖片預(yù)處理成width=x, height=6*x的大小柔昼。
根據(jù)前面兩個(gè)圖哑芹,我們可以推導(dǎo)出最終天空盒的頂點(diǎn)數(shù)據(jù)如下:
// 頂點(diǎn)坐標(biāo), 頂點(diǎn)顏色捕透, 紋理坐標(biāo)聪姿,
// 上面
{{-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
有了以上的頂點(diǎn)數(shù)據(jù)和紋理數(shù)據(jù),我們可以接著
4乙嘀、調(diào)整投影矩陣和模型變換矩陣
這里我們用GLKMatrix4MakeLookAt來(lái)生成模型變換矩陣
// 調(diào)整眼睛的位置
self.eyePosition = GLKVector3Make(2.0f * sinf(angle),
2.0f * cosf(angle),
0.0f);
// 調(diào)整觀察的位置
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); // 模型變換矩陣
這里的眼睛位置就是平截體起點(diǎn)咳燕,觀察方向是指眼睛到遠(yuǎn)平面中心點(diǎn)的方向,如下:
投影矩陣如下乒躺,對(duì)應(yīng)的參數(shù)是上面的視野角、寬高比低缩、近平面距離嘉冒、遠(yuǎn)平面距離。
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(85.0f), aspect, 0.1f, 20.f); // 投影變換矩陣
5咆繁、shader繪制
vertex RasterizerData
vertexShader(uint vertexID [[ vertex_id ]], // 頂點(diǎn)索引
constant LYVertex *vertexArray [[ buffer(LYVertexInputIndexVertices) ]], // 頂點(diǎn)數(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; // 紋理坐標(biāo)
out.pixelColor = vertexArray[vertexID].color; // 頂點(diǎn)顏色讳推,調(diào)試用
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); // 頂點(diǎn)顏色,方便調(diào)試
return float4(colorTex);
}
頂點(diǎn)shader是正常對(duì)頂點(diǎn)進(jìn)行變換處理玩般,紋理坐標(biāo)银觅、頂點(diǎn)顏色讀取buffer的值;
片元shader是從紋理中讀取顏色坏为,為了方便調(diào)試究驴,可以注釋上面的紋理顏色,采用下面的頂點(diǎn)顏色可以快速定位紋理坐標(biāo)匀伏、頂點(diǎn)坐標(biāo)的問(wèn)題洒忧。
注意事項(xiàng)
在繪制正方體的時(shí)候,可以把正方體縮小够颠,整個(gè)放在平截體內(nèi)熙侍,這樣可以看到完整的正方體,便于調(diào)整頂點(diǎn)坐標(biāo)和紋理坐標(biāo)履磨。
此時(shí)需要解決重復(fù)渲染的問(wèn)題蛉抓,常用兩種辦法:
- 方案1、圖元朝向做剔除剃诅;
[renderEncoder setFrontFacingWinding:MTLWindingCounterClockwise];
[renderEncoder setCullMode:MTLCullModeBack];
- 方案2巷送、深度測(cè)試剔除;
// 創(chuàng)建深度緩存
MTLDepthStencilDescriptor *depthStencilDescriptor = [MTLDepthStencilDescriptor new];
depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionLess;
self.depthStencilState = [self.mtkView.device newDepthStencilStateWithDescriptor:depthStencilDescriptor];
// 然后設(shè)置深度測(cè)試
[renderEncoder setDepthStencilState:self.depthStencilState];
實(shí)現(xiàn)過(guò)程還有另外的一個(gè)問(wèn)題综苔,棱角效果太明顯惩系。這個(gè)是因?yàn)樘炜蘸刑∥徊恚芡队暗浇矫娴拿娣e過(guò)小,導(dǎo)致棱角分明堡牡。解決方案是把天空盒的邊長(zhǎng)適當(dāng)放大(不要超過(guò)遠(yuǎn)平面)抒抬,使得天空盒更多區(qū)域能投影到屏幕,減少棱角區(qū)域的面積晤柄。
附錄 ---- 天空盒的另一種簡(jiǎn)單實(shí)現(xiàn)
注意看前文步驟擦剑,shader讀取紋理用的是texture2d
格式,而天空盒還有另外一種方案是通過(guò)立方體紋理textureCube讀取芥颈。
由于篇幅惠勒,不再贅述具體步驟,詳見(jiàn)demo--TextureCube爬坑。
需要注意的是:
1纠屋、紋理加載方案不同,要用-textureCubeDescriptorWithPixelFormat
方法盾计,同時(shí)紋理上傳接口也不相同售担。如下:
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中的紋理坐標(biāo)不同署辉,這里的紋理坐標(biāo)使用的是頂點(diǎn)坐標(biāo)族铆,而之前的方案使用的是頂點(diǎn)的紋理坐標(biāo)。
out.textureCoordinate = vertexArray[vertexID].position.xyz;
注意哭尝,這里使用的是頂點(diǎn)變換前的坐標(biāo)哥攘,如果使用頂點(diǎn)變換后的坐標(biāo),會(huì)導(dǎo)致的現(xiàn)象是視角無(wú)法旋轉(zhuǎn)材鹦。
// 試試代碼改為下面這段
out.textureCoordinate = out.clipSpacePosition.xyz;
總結(jié)
demo嘗試實(shí)現(xiàn)天空盒的效果逝淹,通過(guò)較為復(fù)雜的方式,去更好學(xué)習(xí)天空盒的原理侠姑。
通過(guò)對(duì)頂點(diǎn)创橄、紋理、變換矩陣的處理莽红,能更好掌握?qǐng)D形學(xué)中三維空間的理解妥畏。
具體的代碼在這里。