1. 程式人生 > >Nvidia SDK11 之 PNPatches(細化算法技術學習)

Nvidia SDK11 之 PNPatches(細化算法技術學習)

direct3d borde put 曲面 轉換 模式 就會 rds 數量級

PN-Triangles (也稱作N-patches) 是比較流行的處理粗糙模型細分算法技術,PN-Triangles算法能夠將低分辨率模型轉化為彎曲表面,該表面然後可以被重新繪制成由“高精曲面細分”的三角形所組成的網格,經常借助於Tessellation (曲面細分) 技術創建外觀更加平滑的模型。

在當今遊戲中,我們認為理所當然的大量視覺假象都可以借助此類算法來消除。這些視覺假象包括人物關節處呈現塊狀圖案、汽車輪子呈多邊形外觀以及面部特征粗糙。

技術分享

圖1 曲面細分技術

無需手工輸入,PN-Triangles 可實現遊戲人物的自動平滑。幾何與光照逼真度均能夠得到提升。

DirectX 11 最大新特性 就是融入了Tessellation (曲面細分) 技術,從本質上講,曲面細分技術是一種將多邊形分解成更加細小的碎片以提升幾何逼真度的方法。例如,如果處理一個正方形並將其沿對角線切開,那麽實際上就是將這一正方形“曲面細分”成為兩個三角形。就其本身而言, Tessellation (曲面細分) 並不能提升半點逼真度。例如,在遊戲中,一個正方形被渲染成為兩個三角形還是兩千個三角形都是無關緊要的。只有在使用新三角形來描述新信息時, Tessellation (曲面細分) 才能提升逼真度。

應用曲面細分技術在對基礎模型局部任意一個三角圖形圖元進行細分更細小的三角形圖元的過程中,會生成許多新的控制點,如圖所示b201、b102、b012等這些控制點在流水線作業過程中經過曲面細分技術之後會重新裝配中新的三角形屬性描述圖元信息。

技術分享

圖2 Pn triangle 一個特殊的貝塞爾曲面

置換貼圖(displacement mapping)。也有翻譯成“位移映射”,似乎更準確。位移映射是同凹凸貼圖,法線貼圖,切線貼圖相區別的另一種制造凹凸細節的技術,它使用一個高度貼圖制造出幾何物體表面上點的位置被替換到另一位置的效果。這種效果通常是讓點的位置沿面法線移動一個貼圖中定義的距離。它使得貼圖具備了表現細節和深度的能力,且可以同時允許自我遮蓋,自我投影和呈現邊緣輪廓。

技術分享 技術分享

圖3 置換貼圖應用實例

當一個置換貼圖 (左) 應用到平面上時,所生成的表面 (右) 就會表現出置換貼圖中所編碼的高度信息。

高模與低模+曲面細分+置換貼圖的區別

高模的Triangles和quadrilateral有時候高達幾千萬個面,這樣的模型放到遊戲引擎裏肯定是妥妥跑不動的,高模的雕刻通常網格分布是均勻的,也就意味著一旦雕刻完成,大部分沒有動到的網格就失去了意義,比如一張桌面只在中心位置有個凸起,那麽桌面其余的不影響輪廓的網格就成了廢面。也就是說高模不能決定模型網格頂點的復雜度,比如一塊平面,假如這個平面既不會有動畫需求,也沒有高低起伏,那麽上邊有一百萬個三角面和2兩個三角面是沒有任何差別的。

直接在GPU上渲染高模計算量大,其二以目前世面上遊戲引擎的光照系統精度也不需要這麽高的面數(例如陰影分辨率太低),你會發現往遊戲引擎裏放一個高模和放一個優化好的低模大部分情況下在渲染結果上區別不大,而前後兩者的面數往往差幾個數量級。

技術分享

圖4 幾種貼圖技術差別

那麽遊戲裏邊有沒有比較“廉價”的方案來表現網格頂點復雜度比較高的模型呢?當下相對流程的就是低模+曲面細分+置換貼圖的技術來近似動態的模擬復雜度比較高的高模。即低模通過曲面細分實現更光滑的表面,而由此產生的大量頂點為之後的置換貼圖提供實現的數據基礎。

用高模烘培得到置換貼圖,這張圖決定細分出來的頂點被挪動到什麽位置,細分越高(定點越多),置換貼圖精度越高自然結果也就越接近原始高模。但有個問題,如果初始低模面數非常低,形狀與高模差距過大,那細分出的頂點就會被移動很長的距離,貼圖就會產生較為明顯的拉伸。所以想要以細分+置換貼圖獲得較好的效果,就需要在制作低模時仔細調配低模的模型結構,也就是很多美術所說的“布線”,但即便是如此你得到的結果肯定也會和高模有區別。

置換貼圖和法線貼圖

技術分享

圖5 置換貼圖原理

在粗網格表面上特定點的位移是沿垂直粗網格光滑法線的精細表面一點到無限細分的粗網格(也稱為極限面)對應點之間的距離,即為紅色箭頭所示。

上藍線圖像是極限面。紅色矢量與光滑法線直接對應於粗糙表面,其長度為位移。這種位移是存儲在置換貼圖。平滑的法線不需要存儲,因為它在渲染時是已知的(它只是一個插值的陰影法線)。黑矢量在紅矢量相交的點上是光滑的法線相交的無線細分粗網格細節表面。這些黑法線是存儲在法線貼圖裏。

置換貼圖圖和法線貼圖已經被預編譯並存儲在DDS文件。它們將被綁定到切線空間,所以這個模型可以在實際使用時仍然可以蒙皮和使用動畫,仍然使用相同的法線貼圖和位移貼圖。

由於置換貼圖是關於限制特定細分曲面計算位移,置換貼圖應用於PN片細分不同於Catmull-Clark細分算法。在開發這個樣本的時候,我們找不到一個工具,可以在PN片細分中生成位移圖,所以我們必須開發我們自己的工具。

1 顯示法線(Show Normals)

//*****************************************************************************************************

//* RenderVectorsVS域著色器

///*****************************************************************************************************

技術分享

圖6 demo 截圖

變量定義:

ID3D11Buffer* g_pMeshVertexBuffer;

ID3D11ShaderResourceView* g_pMeshVertexBufferSRV;

ID3D11Buffer* g_pMeshNormalsBuffer;

ID3D11ShaderResourceView* g_pMeshNormalsBufferSRV;

ID3D11Buffer* g_pMeshTangentsBuffer;

ID3D11ShaderResourceView* g_pMeshTangentsBufferSRV;

//The IA will add a vertex id to each vertex for use by shader stages. For each draw call, the vertex id is //incremented by 1. The IA will add a vertex id to each vertex for use by shader stages. For each draw call, //the vertex id is incremented by 1.

float4 RenderVectorsVS(uniform float scale, uint vertexID : SV_VertexID) : SV_Position

{

HSIn_Diffuse output;

int index = vertexID >> 1;

int isOdd = vertexID & 0x1;

float3 position = g_PositionsBuffer.Load(index).xyz;

float4 directionScaled = g_VectorsBuffer.Load(index);

float3 direction = directionScaled.xyz;

//position += direction * directionScaled.w * isOdd;

position += direction * isOdd * scale;

return mul(float4(position, 1.0f), g_ModelViewProjectionMatrix);

}

2 細分著色器

曲面細分階段,細分為3個階段:外殼著色器(Hull - Shader)、Tessellation階段、域著色器階段(Domain - Shader )。一三階段可編程,第二階段不可編程。

技術分享

圖7 細分流程

2.1 頂點著色器

//*****************************************************************************************************

//*RenderTessellatedDiffuseVS域著色器

//*****************************************************************************************************

HSIn_Diffuse RenderTessellatedDiffuseVS(uint vertexID : SV_VertexID, uniform bool renderAnimated = false)

{

//得到頂點的相關屬性信息,頂點著色器之後的頂點patch圖元裝配階段做準備

HSIn_Diffuse output;

int2 indices = g_IndicesBuffer.Load(vertexID);

output.position = g_PositionsBuffer.Load(indices.x).xyz;

output.texCoord = g_CoordinatesBuffer.Load(indices.xy);

float4 normalData = g_NormalsBuffer.Load(indices.x);

output.normal = normalData.xyz;

float4 tangentData = g_TangentsBuffer.Load(indices.x);

output.tangent = tangentData.xyz;

#ifdef FIX_THE_SEAMS

output.cornerCoord = g_CornerCoordinatesBuffer.Load(indices.x);

output.edgeCoord = g_EdgeCoordinatesBuffer.Load(vertexID);

#endif

return output;

}

---》input patch

當渲染Tessellation階段的時候,我們並不把整個low-detail的網格提交到 Input Assembly階段,而是把頂點(控制點)打包(Patches),然後將集合Patch提交給IA。Direct3D支持1~32個控制點的Patch,如下

pd3dDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST);

我們之前輸入的是三角形列表,但在這裏,因為有了Tessellation著色器,所以雖然還是一個三角形,但把它當作patch處理,三個頂點即為patch的三個控制點。三角形可以看成3個控制點,四邊形看成4個控制點。

細分著色器OpenGL參考文章:

http://www.cnblogs.com/magrlemon/p/7290642.html

2.2The Hull Stage (外殼著色器)

//*****************************************************************************************************

//* DiffuseHS 外殼著色器TCS

///*****************************************************************************************************

[domain("tri")] //按triangle 細分

[partitioning("integer")] //細分模式

[outputtopology("triangle_cw")] //細分後輸出的語義

[outputcontrolpoints(3)] //該HS著色器對每個patch調用的次數

[patchconstantfunc("DiffuseConstantHS")] //the constant hull shader函數的名字

[maxtessfactor(64.0)] //最大的細分因子。Direct3D支持最多64個

HSIn_Diffuse DiffuseHS( InputPatch<HSIn_Diffuse, 3> inputPatch, uint i : SV_OutputControlPointID)

{

return inputPatch[i]

}

2.3 DiffuseConstantHS

//*****************************************************************************************************

//* Constant Hull Shader著色器

///*****************************************************************************************************

對每個Patch(可以理解為控制點的集合)進行操作,用來輸出曲面細分因子的,曲面細分因子能告訴給Tessellation階段應該把Patch細分成幾段,根據三次貝塞爾曲面算法生成六個控制點和一個質心偏移坐標。

HS_CONSTANT_DATA_OUTPUT DiffuseConstantHS( InputPatch<HSIn_Diffuse, 3> inputPatch)

{

HS_CONSTANT_DATA_OUTPUT output;

// tessellation factors are proportional to model space edge length

for (uint ie = 0; ie < 3; ++ie)

{

// g_TessellationFactor / (float)512 * (float)64 的值是把這條邊分為幾段

#ifdef MESH_CONSTANT_LOD

output.Edges[ie] = g_TessellationFactor / (float)512 * (float)64;

#else

//Patch包含的三個控制點p0,p1鐘,兩個定點的向量 v0

float3 edge = inputPatch[(ie + 1) % 3].position - inputPatch[ie].position;

//p1 ,p0中點到攝像機的向量

float3 vec = (inputPatch[(ie + 1) % 3].position + inputPatch[ie].position) / 2 - g_FrustumOrigin;

float len = sqrt(dot(edge, edge) / dot(vec, vec));

//該patch中的三個控制點的邊向量長度與攝像機到中點向量長度的比值len實時的設置三條邊的細分因子output.Edges[(ie+1]%3]

output.Edges[(ie + 1) % 3] = max(1, len * g_TessellationFactor);

#endif

}

//****************************************************************************************************************

//*細分過程為幾何提供自動無縫LOD銜接。當表面較遠,它的細分因子很小,在圖像質量明顯沒有下降的情況下,性能提高很多。缺點是當//*靠近幾何模型,細分能夠很好地精細表現,但是性能下降。為了解決這個問題,我們的樣本在HS中使用剔除。當表面接近相機時,只有少//*數可見的貼片——由於使用錐裁剪,大部分貼片最終都看不見了

//***************************************************************************************************************

// culling 背面剔除

int culled[4];

for (int ip = 0; ip < 4; ++ip)

{

culled[ip] = 1;

culled[ip] &= dot(inputPatch[0].position - g_FrustumOrigin, g_FrustumNormals[ip].xyz) > 0;

culled[ip] &= dot(inputPatch[1].position - g_FrustumOrigin, g_FrustumNormals[ip].xyz) > 0;

culled[ip] &= dot(inputPatch[2].position - g_FrustumOrigin, g_FrustumNormals[ip].xyz) > 0;

}

if (culled[0] || culled[1] || culled[2] || culled[3]) output.Edges[0] = 0;

#ifdef PN_TRIANGLES

// compute the cubic geometry control points

// edge control points

備註:三角形三條邊的6個控制點的計算方式都一樣,下面以f3B210為例進行說明。

技術分享

圖8 控制點計算

output.f3B210 = ( ( 2.0f * inputPatch[0].position ) + inputPatch[1].position - ( dot( ( inputPatch[1].position - inputPatch[0].position ), inputPatch[0].normal ) * inputPatch[0].normal ) ) / 3.0f;

output.f3B120 = ( ( 2.0f * inputPatch[1].position ) + inputPatch[0].position - ( dot( ( inputPatch[0].position - inputPatch[1].position ), inputPatch[1].normal ) * inputPatch[1].normal ) ) / 3.0f;

output.f3B021 = ( ( 2.0f * inputPatch[1].position ) + inputPatch[2].position - ( dot( ( inputPatch[2].position - inputPatch[1].position ), inputPatch[1].normal ) * inputPatch[1].normal ) ) / 3.0f;

output.f3B012 = ( ( 2.0f * inputPatch[2].position ) + inputPatch[1].position - ( dot( ( inputPatch[1].position - inputPatch[2].position ), inputPatch[2].normal ) * inputPatch[2].normal ) ) / 3.0f;

output.f3B102 = ( ( 2.0f * inputPatch[2].position ) + inputPatch[0].position - ( dot( ( inputPatch[0].position - inputPatch[2].position ), inputPatch[2].normal ) * inputPatch[2].normal ) ) / 3.0f;

output.f3B201 = ( ( 2.0f * inputPatch[0].position ) + inputPatch[2].position - ( dot( ( inputPatch[2].position - inputPatch[0].position ), inputPatch[0].normal ) * inputPatch[0].normal ) ) / 3.0f;

// center control point

float3 f3E = ( output.f3B210 + output.f3B120 + output.f3B021 + output.f3B012 + output.f3B102 + output.f3B201 ) / 6.0f;

float3 f3V = ( inputPatch[0].position + inputPatch[1].position + inputPatch[2].position ) / 3.0f;

output.f3B111 = f3E + ( ( f3E - f3V ) / 2.0f );

#endif

output.Inside = (output.Edges[0] + output.Edges[1] + output.Edges[2]) / 3;

float2 t01 = inputPatch[1].texCoord - inputPatch[0].texCoord;

float2 t02 = inputPatch[2].texCoord - inputPatch[0].texCoord;

//判斷z軸朝向 向裏還是向外

output.sign = t01.x * t02.y - t01.y * t02.x > 0.0f ? 1 : -1;

return output;

}

//*****************************************************************************************************

//* _RenderPositionAndNormalPS域著色器

///*****************************************************************************************************

貝塞爾三角形是一種特殊的貝塞爾曲面,它通過控制點和質心坐標信息來確定三次曲面上的點的位置,而PN三角形又是貝塞爾三角形的一種特殊的實現,即PN三角形的控制點信息是依據輸入三角形的頂點位置信息和法線信息計算求得,而它的質心坐標則是通過細分著色器來進行插值並輸出。

幾何控制點的計算

技術分享技術分享

圖9 三角形貝塞爾曲面細分示意圖

輸入渲染管線中的三角面片的信息以頂點為單位,如圖 2 所示,P1 ~ P3 是輸入頂點的位置信息,N1 ~ N3是輸入頂點的法線信息基於這些信息可求得控制點信息。如圖3 所示,一個三角面片共有10個控制點,其中 b003,b300,b030是原三角形的 3 個頂點,而其余的7 個控制點則是依據頂點和法線信息插入的,之後根據控制點信息和細分著色器輸出的質心坐標信息共同計算出插入點的位置。

//////////////////////////////////////////////////////////////////////////////////////////////////////////

/// DiffuseDS 域著色器

//////////////////////////////////////////////////////////////////////////////////////////////////////////

[domain("tri")]

PSIn_TessellatedDiffuse DiffuseDS( HS_CONSTANT_DATA_OUTPUT input,

float3 barycentricCoords : SV_DomainLocation,

OutputPatch<HSIn_Diffuse, 3> inputPatch )

{

PSIn_TessellatedDiffuse output;

float3 coordinates = barycentricCoords;

// The barycentric coordinates 質心就是面積坐標

float fU = barycentricCoords.x;

float fV = barycentricCoords.y;

float fW = barycentricCoords.z;

// Precompute squares and squares * 3

float fUU = fU * fU;

float fVV = fV * fV;

float fWW = fW * fW;

float fUU3 = fUU * 3.0f;

float fVV3 = fVV * 3.0f;

float fWW3 = fWW * 3.0f;

技術分享

參考模型二

技術分享

技術分享

註意如下:

技術分享

‘u/v/w’ 是質心坐標(他們始終滿足等式:u + v + w = 1),‘Bxyz’ 是一組控制點

正如你所見的那樣,一組控制點大體就是三角形表面上的一個膨脹表面,將質心坐標帶入上面的這個公式,我們就能得到更加接近真實的 3D 表面。

技術分享

圖10 Bezier 三角形面片

// Compute position from cubic control points and barycentric cords

//三角形上的貝塞爾曲面計算公式

float3 position = inputPatch[0].position * fWW * fW + inputPatch[1].position * fUU * fU + inputPatch[2].position * fVV * fV +input.f3B210 * fWW3 * fU + input.f3B120 * fW * fUU3 + input.f3B201 * fWW3 * fV + input.f3B021 * fUU3 * fV +input.f3B102 * fW * fVV3 + input.f3B012 * fU * fVV3 + input.f3B111 * 6.0f * fW * fU * fV;

// Compute normal from quadratic control points and barycentric cords

// 面積坐標的比例因子計算重心的法線

float3 normal = inputPatch[0].normal * coordinates.z + inputPatch[1].normal * coordinates.x + inputPatch[2].normal * coordinates.y;

normal = normalize(normal);

// 面積坐標的比例因子計算重心的紋理坐標

float2 texCoord = inputPatch[0].texCoord * coordinates.z + inputPatch[1].texCoord * coordinates.x + inputPatch[2].texCoord * coordinates.y;

float2 displacementTexCoord = texCoord;

#ifdef FIX_THE_SEAMS

// Edge point 特殊情況

//當質心坐標在三角形的一條邊上,coordinates.z對應邊的起點到邊的終點采用面積坐標比例coordinates.y 或者

//(1- coordinates.y)進行插值。

if(coordinates.z == 0)

displacementTexCoord = lerp(inputPatch[1].edgeCoord.xy, inputPatch[1].edgeCoord.zw, coordinates.y);

else if(coordinates.x == 0)

displacementTexCoord = lerp(inputPatch[2].edgeCoord.xy, inputPatch[2].edgeCoord.zw, coordinates.z);

else if(coordinates.y == 0)

displacementTexCoord = lerp(inputPatch[0].edgeCoord.xy, inputPatch[0].edgeCoord.zw, coordinates.x);

// Corner point特殊情況

//當質心坐標在三角形的頂點上,由面積坐標計算而得

if(coordinates.z == 1)

displacementTexCoord = inputPatch[0].cornerCoord;

else if(coordinates.x == 1)

displacementTexCoord = inputPatch[1].cornerCoord;

else if(coordinates.y == 1)

displacementTexCoord = inputPatch[2].cornerCoord;

#endif

//采用置換貼圖,偏移值保存在x變量,面積坐標可能是三角形inputPatch[0],inputPatch[1],inputPatch[2]中的任意一點。

#ifndef IGNORE_DISPLACEMENT

float offset = g_DisplacementTexture.SampleLevel(SamplerLinearClamp, displacementTexCoord, 0).x;

position += normal * offset;

#endif

//計算重心坐標的切線坐標

float3 tangent = inputPatch[0].tangent * coordinates.z + inputPatch[1].tangent * coordinates.x + inputPatch[2].tangent * coordinates.y;

tangent = normalize(tangent);

//齊次投影坐標空間

output.position = mul(float4(position, 1.0f), g_ModelViewProjectionMatrix);

#ifdef SMOOTH_TCOORDS

output.texCoord = displacementTexCoord;

#else

output.texCoord = texCoord;

#endif

output.positionWS = position;

output.normal = normal;

output.tangent = tangent;

output.sign = input.sign;//inputPatch[0].sign;

return output;

}

//////////////////////////////////////////////////////////////////////////////////////////////////////////

/// DiffuseDS 域著色器

//////////////////////////////////////////////////////////////////////////////////////////////////////////

struct PSIn_TessellatedDiffuse

{

float4 position : SV_Position;

float2 texCoord : TEXCOORD0;

float3 positionWS : TEXCOORD1;

float3 normal : TEXCOORD2;

float3 tangent : TEXCOORD3;

float sign : TEXCOROD4;

};

float4 RenderTessellatedDiffusePS(PSIn_TessellatedDiffuse input) : SV_Target

{

#ifdef FLAT_NORMAL

// gpu的pixel shader處理的像素,每次都是一個2x2的quad,對於任意一個屬性在rtx(RenderTarget x direction)或//者rty方向上的偏導數,都是可計算的, 因為數據是離散的,所以偏導數的計算就是簡單的相減 使用ddx/ddy,切記一定//要確保其2x2區域位於同一三角面的光柵化範圍內

float3 dir_x = ddx(input.positionWS);

float3 dir_y = ddy(input.positionWS);

float3 normal = normalize(cross(dir_x, dir_y));

float3 lightDir = normalize(g_CameraPosition - input.positionWS);

#else

float3 normal = normalize(input.normal);

float3 tangent = normalize(input.tangent);

//參考圖5 求副法線

float3 bitangent = cross(normal, tangent) * input.sign;

// 構建變換矩陣,將位置坐標從模型空間轉換到切線空間

float3x3 tangentBasis = float3x3(tangent, bitangent, normal);

float3 lightDir = normalize(g_CameraPosition - input.positionWS);

// 轉換光源方向從模型空間到切線空間

lightDir = normalize(mul(tangentBasis, lightDir));

//采樣獲取法線紋理值

normal = normalize(g_WSNormalMap.Sample(SamplerLinearClamp, input.texCoord).xyz);

#endif

//在切線空間坐標系下求得該頂點的受光影響

float dotNL = max(dot(normal, lightDir), 0.0f);

float diffuse = dotNL * 0.75f + 0.25f;

float specular = pow(dotNL, 100.0f);

float3 diffuseColor = float3(1.0, 0.5, 0.35) * 0.75;// * 0.75 + (input.sign * 0.5 + 0.5) * 0.25;

float3 color = diffuseColor * diffuse + specular * 0.25;

return float4(color, 0);

}

Nvidia SDK11 之 PNPatches(細化算法技術學習)