Tessellation——中文一般譯作“細分曲面”,一般用於將由少量頂點構成的面生成細節度更高的面。這其中的原理是將一個三角形或四邊形,由GPU根據我們編程的控制點生成規則,自動生成更多的頂點,然後將這些頂點根據一定規則生成更多的三角形。這麼一來,我們可以在3D游戲中在遠處的敵人使用低模也能做出精細度較高的模型出來了,而且也省頂點數據傳輸帶寬。
在Metal API中,通過tessellation繪制出的圖形所走的渲染流水線會與通過傳統的頂點著色器所走的渲染流水線會有所不同。Metal API為了簡化本身Metal Shading Language的語法,將已有的Compute Shader用來計算控制點生成規則,然後將結果送給Tesselator(曲面細分器)生成具體的控制點,最後交給用於細分曲面後處理的Vertex Shader做頂點數據輸出,然後後面的就跟典型的渲染流水線一樣處理了。
上圖中,藍色圓角矩形部分是可編程著色器;綠色圓角矩形部分是GPU的固定硬件單元;灰色部分是數據緩存對象。我們可以看到,走Tessellation流水線相對於傳統的渲染流水線,其實也就多了計算著色器(用於生成每個patch的細分曲面因子)以及曲面細分器單元。所謂patch其實與一般的三角形(triangle)與四邊形(quad)差不多,只是在用於細分曲面的處理過程中,一個patch是在一個平面裡的,所以它沒有z坐標,我們在細分曲面後處理的頂點著色器中可以給轉換後的頂點再做視圖模型變換以及投影變換,然後輸出。在Metal API中,細分曲面只支持三角形與四邊形,不支持線段;在OpenGL中還支持線段。
對於四邊形而言,patch的頂點排列與普通的矩形會有些差別。我們通常用Metal API繪制一個矩形往往會用triangle strip的繪制模式(由於Metal API中沒有triangle fan的繪制模式),因此一個矩形的頂點排列可以是:左上,左下,右上,右下這種排列方式。然而繪制patch則不能用這種方式,我們只能對四個頂點依次以順時針或逆時針的順序進行排列。四邊形patch的控制點位於四邊形的邊緣以及四邊形的內部,因此用於描述四邊形patch控制點的結構體定義如下:
typedefstruct{/*NOTE:edgeTessellationFactorandinsideTessellationFactorareinterpretedashalf(16-bitfloats)*/
uint16_tedgeTessellationFactor[4];uint16_tinsideTessellationFactor[2];
}MTLQuadTessellationFactorsHalf; 該結構體中的edgeTessellationFactor成員用於描述四邊形四個邊緣的控制點生成情況;insideTessellationFactor用於描述內部控制點的生成情況。patch頂點的排列不同,對於edgeTessellationFactor數組對象的各個元素所對應的哪條邊也是有所不同的。本文中,我們編排四邊形patch的頂點順序依次為四邊形的左下頂點、右下頂點、右上頂點、左上頂點,以逆時針的次序進行編排。這樣,edgeTessellationFactor[0]對應的是左邊,edgeTessellationFactor[1]對應的是下邊,edgeTessellationFactor[2]對應的是右邊,edgeTessellationFactor[3]對應的是上邊。請見此圖:點擊看圖[objc] view plain copy
typedefstruct{/*NOTE:edgeTessellationFactorandinsideTessellationFactorareinterpretedashalf(16-bitfloats)*/
uint16_tedgeTessellationFactor[3];uint16_tinsideTessellationFactor;
}MTLTriangleTessellationFactorsHalf;
為了統一描述,本文將三角形patch的頂點編排次序排列為:右下、中上、左下。這樣使得這裡的edgeTessellationFactor[0]對應於三角形的左邊,edgeTessellationFactor[1]對應於三角形的下邊,edgeTessellationFactor[2]對英語三角形的右邊。我們見此圖所示:點擊看圖
三角形邊緣上控制點的生成與四邊形的類似,而內部控制點生成規則就要復雜不少了。對於insideTessellationFactor值,如果它小於3,那麼在三角形內就只有一個控制點,坐落於三角形的重心(所謂三角形重心,即三角形的三個頂點到對邊上的中線所交匯的點)處。如果是3,那麼在三角形內部則正好有3個控制點構成一個小三角形。如果是4,那麼在小三角形中再增加一個控制點,坐落於小三角形的重心處,並且小三角形的每個邊緣的中點處增加一個控制點。如果是5,內部小三角形的每條邊緣則含有兩個控制點,均分邊緣;並且在小三角形內部再次構成一個小三角形。如果是6,小三角形的邊緣含有3個控制點均分邊緣;小三角形內部的小三角形的每個邊緣上增加1個控制點,並且在其內部再增加一個控制點,以此類推……具體算法以及相關的圖我們可以參考OpenGL官網wiki上關於Tessellation的介紹:https://www.opengl.org/wiki/Tessellation。
三角形patch的每個控制點的坐標以(u, v, w)進行描述,u、v和w的取值均為[0.0, 1.0]區間內。以本文的三角形patch頂點編排順序為例,三角形左邊的所有控制點的u坐標為0;三角形下邊上的所有控制點的v坐標為0,三角形右邊上所有控制點的w坐標為0。那麼在三角形內部的某一個控制點的坐標如何計算得到呢?這裡比較暈乎的是在一個二維平面中用三個坐標值來表示一個點,不過這裡有意思的一點是:對於三角形內的任一控制點的(u, v, w)坐標,u + v + w = 1.0。所以它們之間是具有關聯性的。我們可以從一個簡單的例子進行著手分析——對於一個等邊三角形,我們假設其中線(對於等邊三角形,三線合一,即中線、垂直線、角平分線,另外中心與重心也是同一個)長度為1.0,那麼其重心到三條邊中點的距離正好都是1/3,那麼我們知道,如果將位於重心的控制點的坐標定義為(1/3, 1/3, 1/3),那麼u + v + w的值正好為1.0。所以我們判定三角形patch內部某個控制點的坐標可以這麼做:對於判定u值,將三角形左邊沿著其中線進行平移,直到與控制點重合,然後看平移後的左邊與左邊中線的相交點,該點在此中線上的位置即為u值;判斷v值也類似,將三角形下邊沿著其中線進行平移,然後正好與該控制點重合處,看該下邊與其中線相交點在此中線上的位置;w點則是看右邊沿著其中線平移的情況。
有了以上對於細分曲面控制點生成的相關概念之後,下面我們就開始描述Metal API中完成細分曲面渲染的整個過程了。
我們首先看如何用計算著色器設置控制點的生成規則。計算著色器的實現如下所示:
constantint4tessOutFactors[[function_constant(0)]];constantint2tessInnerFactors[[function_constant(1)]];
//三角形用於生成控制點的計算內核kernelvoidtriangle_kernel(devicestruct
MTLTriangleTessellationFactorsHalf*factors[[buffer(0)]],uinttid[[thread_position_in_grid]])
{//Simplepassthroughoperation
//Moresophisticatedcomputekernelsmightdeterminethetessellationfactorsbasedonthestateofthescene(e.g.cameradistance)factors[tid].edgeTessellationFactor[0]=tessOutFactors.x;
factors[tid].edgeTessellationFactor[1]=tessOutFactors.y;factors[tid].edgeTessellationFactor[2]=tessOutFactors.z;
factors[tid].insideTessellationFactor=tessInnerFactors.x;}
//四邊形用於生成控制點的計算內核
kernelvoidquad_kernel(devicestructMTLQuadTessellationFactorsHalf*factors[[buffer(0)]],
uinttid[[thread_position_in_grid]]){
//Simplepassthroughoperation//Moresophisticatedcomputekernelsmightdeterminethetessellationfactorsbasedonthestateofthescene(e.g.cameradistance)
factors[tid].edgeTessellationFactor[0]=tessOutFactors.x;factors[tid].edgeTessellationFactor[1]=tessOutFactors.y;
factors[tid].edgeTessellationFactor[2]=tessOutFactors.z;factors[tid].edgeTessellationFactor[3]=tessOutFactors.w;
factors[tid].insideTessellationFactor[0]=tessInnerFactors.x;factors[tid].insideTessellationFactor[1]=tessInnerFactors.y;
}
上述Metal Shading Language片段中,最上面的兩個常量值將由主機端傳入,tessOutFactors是一個四元素向量,每個分量值對應edgeTessellationFactor的相應元素值,這一點我們在兩個內核函數中也能看得很清楚。而tessInnerFactors則是一個二元素向量,對於四邊形,其第一個分量作為insideTessellationFactor的第0個元素值,第二個分量作為insideTessellationFactor的第1個元素值;對於三角形,tessInnerFactors的第一個分量作為insideTessellationFactor的值。內核函數的參數就是我們上文提到過的指向MTLTriangleTessellationFactorsHalf結構體或MTLQuadTessellationFactorsHalf結構體數組的指針。每一個此結構體對象表征了對某一個patch進行控制點生成的規則,如果我們有多個patch需要做曲面細分,那麼這裡將會有多個元素,如果就對一個patch進行曲面細分,那麼這裡其實也就一個元素,我們可以在主機端進行控制給計算內核調度多少個線程進行執行。
以下代碼片段是主機端對計算著色器的相關設置:
//0號對應外部邊緣的tess。對於四邊形,順序為左、下、右、上;對於三角形,順序為:左、下、右[constantValuessetConstantValue:(constint[]){5,4,3,2}type:MTLDataTypeInt4atIndex:0];
//1號對應內部的tess。對於四邊形,分別為水平方向tess、垂直方向tess;三角形只能用一個值
[constantValuessetConstantValue:(constint[]){4,3}type:MTLDataTypeInt2atIndex:1];
computeProgram=[mLibrarynewFunctionWithName:tessControlKernelNameconstantValues:constantValueserror:NULL];if(computeProgram==nil)
{NSLog(@"計算內核獲取失敗");
break;}
[constantValuesrelease];
mComputePipelineState=[devicenewComputePipelineStateWithFunction:computeProgramerror:NULL];
if(mComputePipelineState==nil){
NSLog(@"計算流水線創建失敗");break;
}
//由於細分曲面因子緩存最終由生成控制點的內核程序生成,因此不會被CPU讀寫,而是直接在流水線內部使用,因此這裡使用GPU私有存儲模式mTessFactorsBuffer=[devicenewBufferWithLength:256options:MTLResourceStorageModePrivate];
上述代碼片段設置了上面所提到的Metal Shading Language中的常量向量對象,另外創建了計算著色器程序,並建立了計算流水線。mTessFactorsBuffer這個緩存對象中就將存放著色器中的MTLTriangleTessellationFactorsHalf結構體或MTLQuadTessellationFactorsHalf結構體的數據。由於我們這裡僅僅對一個patch進行繪制,所以就假定其長度為256個字節,這對於存放這倆結構體的內容是綽綽有余了。
下面我們看看如何調度執行計算著色器。
idcommandBuffer=[mCommandQueuecommandBuffer];
//設置命令緩存執行完成後的處理[commandBufferaddCompletedHandler:^void(idcmdBuf){
//命令全都執行完之後,將mCurrentDrawable置空,表示可以繪制下面一幀mCurrentDrawable=nil;
}];
//從命令緩存對象獲取計算命令編碼器idcomputeCommandEncoder=[commandBuffercomputeCommandEncoder];
//設置計算流水線
[computeCommandEncodersetComputePipelineState:mComputePipelineState];
//設置內核程序的緩存參數[computeCommandEncodersetBuffer:mTessFactorsBufferoffset:0atIndex:0];
//分派線程組
//由於我們這裡僅對一個patch做控制點生成規則,因此只需要一個線程進行處理即可[computeCommandEncoderdispatchThreadgroups:MTLSizeMake(1,1,1)threadsPerThreadgroup:MTLSizeMake(1,1,1)];
[computeCommandEncoderendEncoding];
上述代碼片段首先獲得了命令緩存對象,然後通過該命令緩存對象獲取計算命令編碼器,對它設置計算流水線以及相關緩存對象。然後我們用計算命令編碼器對計算內核進行分派,這樣就激活了計算著色器的計算執行,然後結束命令編碼器。在Metal中,一個命令緩存對象可以有多個命令編碼器對其進行操作,一般情況下,同一時段只能由一個命令編碼器處於活動狀態,然後一直到它結束編碼為止,另一個命令編碼器才能操作。等到所有命令編碼器完成操作之後,命令緩存可以做提交操作,讓GPU做真正的命令執行。
這裡對於計算著色器而言,MTLTriangleTessellationFactorsHalf結構體或MTLQuadTessellationFactorsHalf結構體數組其實是作為輸出,並且該結構體類型是固定的,我們不要用其他類型來代替它,不過我們可以再添加其他緩存對象來輸入一些必要的參數。
控制點生成規則數據創建完之後,它是怎麼被傳到細分曲面器tessellator中的呢?首先,計算著色器計算好的MTLTriangleTessellationFactorsHalf結構體或MTLQuadTessellationFactorsHalf結構體的數據都是直接存入主機端所創建的mTessFactorsBuffer緩存對象中的。然後,我們將mTessFactorsBuffer緩存對象傳遞給渲染命令編碼器,最後通過渲染編碼器調用drawPatches接口來激活tessellator,並執行後續的渲染流水線。
我們在看細分曲面後處理頂點著色器之前,先看一下主機端的一些設置。首先,我們看一下我們所指定的四邊形與三角形patch的頂點排列:
//以下以逆時針方向構成一個四邊形,使得四邊形的邊順序依次為左、下、右、上。staticconstfloatsRectangleVertices[]={
//左下頂點-0.9f,-0.9f,0.9f,0.1f,0.1f,1.0f,
//右下頂點
0.9f,-0.9f,0.1f,0.9f,0.1f,1.0f,
//右上頂點0.9f,0.9f,0.1f,0.1f,0.9f,1.0f,
//左上頂點
-0.9f,0.9f,0.9f,0.9f,0.1f,1.0f,};
//以下以逆時針方向構成一個三角形,使得三角形的邊順序依次為左、下、右。
staticconstfloatsTriangleVertices[]={
//右下頂點0.9f,-0.9f,0.1f,0.1f,0.9f,1.0f,
//中上頂點
0.0f,0.9f,0.9f,0.1f,0.1f,1.0f,
//左下頂點-0.9f,-0.9f,0.1f,0.9f,0.1f,1.0f,
};
這兩個patch的頂點分布與我們一開始所提到的頂點分布順序一致。各位如果看Apple官方的《The Metal Programming Guide》中的例子的話,其中那張四邊形patch的圖是以:左上、右上、右下、左下的順時針次序排列而得到的。而我們這裡都采用逆時針排列方式。下面是對渲染流水線相關的設置:
vertexProgram=[mLibrarynewFunctionWithName:tessEvalVertexName];if(vertexProgram==nil)
{NSLog(@"頂點著色器獲取失敗");
break;}
fragmentProgram=[mLibrarynewFunctionWithName:@"square_fragment"];
if(fragmentProgram==nil){
NSLog(@"片段著色器獲取失敗");break;
}
MTLVertexDescriptor*vertexDescriptor=[MTLVertexDescriptornew];vertexDescriptor.attributes[0].format=MTLVertexFormatFloat2;
vertexDescriptor.attributes[0].bufferIndex=0;//頂點結構體的此屬性對應的buffer索引為0vertexDescriptor.attributes[0].offset=0;
vertexDescriptor.attributes[1].format=MTLVertexFormatFloat4;vertexDescriptor.attributes[1].bufferIndex=0;//頂點結構體的此屬性對應的buffer索引為0
vertexDescriptor.attributes[1].offset=8;//第二個屬性color偏移是從第8字節開始
vertexDescriptor.layouts[0].stride=6*sizeof(float);vertexDescriptor.layouts[0].stepFunction=MTLVertexStepFunctionPerPatchControlPoint;
vertexDescriptor.layouts[0].stepRate=1;
MTLRenderPipelineDescriptor*descriptor=[MTLRenderPipelineDescriptornew];
//Configurecommonrenderpropertiesdescriptor.sampleCount=4;//我們將使用多重采樣抗鋸齒(MSAA),每個像素由4個樣本構成
descriptor.vertexFunction=vertexProgram;descriptor.fragmentFunction=fragmentProgram;
descriptor.depthAttachmentPixelFormat=MTLPixelFormatInvalid;//不啟用深度測試descriptor.stencilAttachmentPixelFormat=MTLPixelFormatInvalid;//不啟用模版測試
descriptor.vertexDescriptor=vertexDescriptor;[vertexDescriptorrelease];
descriptor.colorAttachments[0].pixelFormat=self.pixelFormat;
descriptor.colorAttachments[0].blendingEnabled=YES;//將渲染流水線設置為允許顏色混合descriptor.colorAttachments[0].rgbBlendOperation=MTLBlendOperationAdd;
descriptor.colorAttachments[0].alphaBlendOperation=MTLBlendOperationAdd;descriptor.colorAttachments[0].sourceRGBBlendFactor=MTLBlendFactorSourceAlpha;
descriptor.colorAttachments[0].sourceAlphaBlendFactor=MTLBlendFactorSourceAlpha;descriptor.colorAttachments[0].destinationRGBBlendFactor=MTLBlendFactorOneMinusSourceAlpha;
descriptor.colorAttachments[0].destinationAlphaBlendFactor=MTLBlendFactorOneMinusSourceAlpha;
//配置細分曲面常用屬性descriptor.tessellationFactorScaleEnabled=NO;
descriptor.tessellationFactorFormat=MTLTessellationFactorFormatHalf;descriptor.tessellationControlPointIndexType=MTLTessellationControlPointIndexTypeNone;
descriptor.tessellationFactorStepFunction=MTLTessellationFactorStepFunctionConstant;//由於頂點構成是以逆時針作為順序的,因此這裡使用逆時針方向
descriptor.tessellationOutputWindingOrder=MTLWindingCounterClockwise;descriptor.tessellationPartitionMode=MTLTessellationPartitionModeInteger;
//在macOS中,最大細分曲面因子為64,默認為16
descriptor.maxTessellationFactor=64;
mRenderPipelineState=[devicenewRenderPipelineStateWithDescriptor:descriptorerror:NULL];[descriptorrelease];
if(mRenderPipelineState==nil){
NSLog(@"渲染流水線創建失敗");break;
}
if(mIsForRectangle)mVertexBuffer=[devicenewBufferWithBytes:sRectangleVerticeslength:sizeof(sRectangleVertices)options:MTLResourceCPUCacheModeWriteCombined];
elsemVertexBuffer=[devicenewBufferWithBytes:sTriangleVerticeslength:sizeof(sTriangleVertices)options:MTLResourceCPUCacheModeWriteCombined];
各位這裡要注意的是,對於細分曲面後處理的頂點著色器,原patch的頂點必須以stage-in的方式傳入,而不能是一個普通的數據緩存對象。此外,在Metal Shading Language中,我們必須用patch_control_point結構體模板對輸入的patch結構體進行封裝。細分曲面後處理頂點著色器函數逐個對patch頂點數據進行讀入,而我們這裡必須使用patch_control_point結構體對象作為輸入參數。此外,我們看到上述頂點描述符中,其layouts[0]的stepFunction成員必須用MTLVertexStepFunctionPerPatchControlPoint,表示該頂點將作為patch控制點來使用。關於tessellationPartitionMode模式的詳細介紹,各位可以參考http://www.cnblogs.com/zenny-chen/p/4280100.html此博文中“指定細分曲面坐標的空間”部分,這裡使用MTLTessellationPartitionModeInteger表示均分。創建patch頂點緩存的方式與創建普通圖形頂點緩存的方式一樣。
下面我們來看一下如何渲染patch。
[objc] view plain copy
//從命令緩存對象獲取渲染命令編碼器idrenderCommandEncoder=[commandBufferrenderCommandEncoderWithDescriptor:mRenderPassDescriptor];
[renderCommandEncodersetRenderPipelineState:mRenderPipelineState];
[renderCommandEncodersetCullMode:MTLCullModeBack];[renderCommandEncodersetFrontFacingWinding:MTLWindingCounterClockwise];
[renderCommandEncodersetTriangleFillMode:mIsWireFrameMode?MTLTriangleFillModeLines:MTLTriangleFillModeFill];
[renderCommandEncodersetVertexBuffer:mVertexBufferoffset:0atIndex:0];[renderCommandEncodersetTessellationFactorBuffer:mTessFactorsBufferoffset:0instanceStride:0];
constNSUIntegerpatchControlPoints=mIsForRectangle?4:3;
[renderCommandEncoderdrawPatches:patchControlPointspatchStart:0patchCount:1patchIndexBuffer:NULLpatchIndexBufferOffset:0instanceCount:1baseInstance:0];
[renderCommandEncoderendEncoding];
[commandBufferpresentDrawable:mCurrentDrawable];
[commandBuffercommit]; 這裡我們調用渲染命令編碼器的setTessellationFactorBuffer接口,將我們在計算著色器中輸出的存放細分曲面因子數據的mTessFactorBuffer緩存對象傳遞進去。這個對象不需要在Metal Shader中作為參數傳遞進去,因為它直接會交給tessellator固定功能單元,而在細分曲面後處理頂點著色器中我們不需要關心這些數據,我們得到的是將是tessellator幫我們生成好的具體控制點信息。這裡大家需要注意的是,我們應該先讓計算命令編碼器活動工作,結束後再使用渲染命令編碼器,最後提交命令緩存。
下面給出細分曲面後處理頂點著色器:
view plaincopy
//控制點結構體structControlPoint{
float2position[[attribute(0)]];float4color[[attribute(1)]];
};
//Patch結構體structPatchIn{
patch_control_pointcontrol_points;};
//頂點著色器輸出到片段著色器的結構體
structFunctionOutIn{float4position[[position]];
half4color[[flat]];};
//三角形細分曲面後處理頂點著色器
[[patch(triangle,3)]]vertexstructFunctionOutIntriangle_vertex(structPatchInpatchIn[[stage_in]],
float3patch_coord[[position_in_patch]]){
floatu=patch_coord.x;floatv=patch_coord.y;
floatw=patch_coord.z;
//將當前控制點坐標(u,v,w)通過線性插值轉換為笛卡爾坐標(x,y)floatx=u*patchIn.control_points[0].position.x+v*patchIn.control_points[1].position.x+w*patchIn.control_points[2].position.x;
floaty=u*patchIn.control_points[0].position.y+v*patchIn.control_points[1].position.y+w*patchIn.control_points[2].position.y;
//頂點輸出structFunctionOutInvertexOut;
vertexOut.position=float4(x,y,0.0,1.0);vertexOut.color=half4(u,v,w,1.0);
returnvertexOut;}
//四邊形細分曲面後處理頂點著色器
[[patch(quad,4)]]vertexstructFunctionOutInquad_vertex(structPatchInpatchIn[[stage_in]],
float2patch_coord[[position_in_patch]]){
//從tessellator處理之後所獲得的規格化之後的控制點坐標——//uv坐標的原點(即(0,0)的位置)是在原patch的左下頂點
floatu=patch_coord.x;floatv=patch_coord.y;
//以下通過線性插值的算法將規格化後的控制點坐標再轉換為相對於輸入頂點的坐標
float2lower_middle=mix(patchIn.control_points[0].position.xy,patchIn.control_points[1].position.xy,u);float2upper_middle=mix(patchIn.control_points[2].position.xy,patchIn.control_points[3].position.xy,1-u);
//頂點輸出
structFunctionOutInvertexOut;vertexOut.position=float4(mix(lower_middle,upper_middle,v),0.0f,1.0f);
vertexOut.color=half4(u,v,1.0f-v,1.0h);
//靠左下的所有頂點使用原patch左下頂點的顏色if(u<0.5f&&v<0.5f)
vertexOut.color=half4(patchIn.control_points[0].color);//靠右下的所有頂點使用原patch右下頂點的顏色
elseif(u>0.5f&&v<0.5f)vertexOut.color=half4(patchIn.control_points[1].color);
//靠右上的所有頂點使用原patch右上頂點的顏色elseif(u>0.5f&&v>0.5f)
vertexOut.color=half4(patchIn.control_points[2].color);//靠左上的所有頂點使用原patch左上頂點的顏色
elseif(u<0.5f&&v>0.5f)vertexOut.color=half4(patchIn.control_points[3].color);
returnvertexOut;
} 上述代碼片段中,我們看到,細分曲面後處理頂點著色器通過[[ patch() ]]屬性進行修飾來描述,在macOS中,我們必須指定當前patch頂點個數,而在iOS中則可缺省。在細分曲面後處理頂點著色器中,patch_coord參數存放的是來自tessellator的當前控制點的相對坐標(四邊形為(u, v),三角形為(u, v, w))。我們在此著色器中就需要將這些控制點坐標轉換為我們標准笛卡爾坐標系下的坐標值,也就是與原patch頂點坐標相一致的坐標系的值。在四邊形的後處理著色器中,我們還通過判定控制點所在的區域,分別給它們設置我們在頂點數據中所包含的顏色數據。最後,vertexOut的輸出就最終給光柵化器,然後交給片段著色器。
最後,如果此博文有些疏漏或表達不准確的地方也請其他大牛不吝指出,我也會做相應修改,謝謝。