PN-Triangles (也稱做N-patches) 是比較流行的處理粗糙模型細分算法技術,PN-Triangles算法可以將低分辨率模型轉化爲彎曲表面,該表面而後能夠被從新繪製成由「高精曲面細分」的三角形所組成的網格,常常藉助於Tessellation (曲面細分) 技術建立外觀更加平滑的模型。html
在當今遊戲中,咱們認爲理所固然的大量視覺假象均可以藉助此類算法來消除。這些視覺假象包括人物關節處呈現塊狀圖案、汽車輪子呈多邊形外觀以及面部特徵粗糙。算法
圖1 曲面細分技術編程
無需手工輸入,PN-Triangles 可實現遊戲人物的自動平滑。幾何與光照逼真度均可以獲得提高。app
DirectX 11 最大新特性 就是融入了Tessellation (曲面細分) 技術,從本質上講,曲面細分技術是一種將多邊形分解成更加細小的碎片以提高几何逼真度的方法。例如,若是處理一個正方形並將其沿對角線切開,那麼實際上就是將這一正方形「曲面細分」成爲兩個三角形。就其自己而言, Tessellation (曲面細分) 並不能提高半點逼真度。例如,在遊戲中,一個正方形被渲染成爲兩個三角形仍是兩千個三角形都是可有可無的。只有在使用新三角形來描述新信息時, Tessellation (曲面細分) 才能提高逼真度。dom
應用曲面細分技術在對基礎模型局部任意一個三角圖形圖元進行細分更細小的三角形圖元的過程當中,會生成許多新的控制點,如圖所示b20一、b10二、b012等這些控制點在流水線做業過程當中通過曲面細分技術以後會從新裝配中新的三角形屬性描述圖元信息。ide
圖2 Pn triangle 一個特殊的貝塞爾曲面函數
置換貼圖(displacement mapping)。也有翻譯成「位移映射」,彷佛更準確。位移映射是同凹凸貼圖,法線貼圖,切線貼圖相區別的另外一種製造凹凸細節的技術,它使用一個高度貼圖製造出幾何物體表面上點的位置被替換到另外一位置的效果。這種效果一般是讓點的位置沿面法線移動一個貼圖中定義的距離。它使得貼圖具有了表現細節和深度的能力,且能夠同時容許自我遮蓋,自我投影和呈現邊緣輪廓。工具
圖3 置換貼圖應用實例性能
當一個置換貼圖 (左) 應用到平面上時,所生成的表面 (右) 就會表現出置換貼圖中所編碼的高度信息。優化
高模與低模+曲面細分+置換貼圖的區別
高模的Triangles和quadrilateral有時候高達幾千萬個面,這樣的模型放到遊戲引擎裏確定是妥妥跑不動的,高模的雕刻一般網格分佈是均勻的,也就意味着一旦雕刻完成,大部分沒有動到的網格就失去了意義,好比一張桌面只在中心位置有個凸起,那麼桌面其他的不影響輪廓的網格就成了廢面。也就是說高模不能決定模型網格頂點的複雜度,好比一塊平面,假如這個平面既不會有動畫需求,也沒有高低起伏,那麼上邊有一百萬個三角面和2兩個三角面是沒有任何差異的。
直接在GPU上渲染高模計算量大,其二以目前世面上游戲引擎的光照系統精度也不須要這麼高的面數(例如陰影分辨率過低),你會發現往遊戲引擎裏放一個高模和放一個優化好的低模大部分狀況下在渲染結果上區別不大,而先後二者的面數每每差幾個數量級。
圖4 幾種貼圖技術差異
那麼遊戲裏邊有沒有比較「廉價」的方案來表現網格頂點複雜度比較高的模型呢?當下相對流程的就是低模+曲面細分+置換貼圖的技術來近似動態的模擬複雜度比較高的高模。即低模經過曲面細分實現更光滑的表面,而由此產生的大量頂點爲以後的置換貼圖提供實現的數據基礎。
用高模烘培獲得置換貼圖,這張圖決定細分出來的頂點被挪動到什麼位置,細分越高(定點越多),置換貼圖精度越高天然結果也就越接近原始高模。但有個問題,若是初始低模面數很是低,形狀與高模差距過大,那細分出的頂點就會被移動很長的距離,貼圖就會產生較爲明顯的拉伸。因此想要以細分+置換貼圖得到較好的效果,就須要在製做低模時仔細調配低模的模型結構,也就是不少美術所說的「佈線」,但即使是如此你獲得的結果確定也會和高模有區別。
置換貼圖和法線貼圖
圖5 置換貼圖原理
粗網格表面上特定點的位移大小取決於粗網格的對應曲面上的光滑法線到無限劃分的精細網格(也稱爲極限面)對應點之間的距離大小,即爲紅色箭頭所示。
上藍線圖像是極限面。紅色矢量與光滑法線直接對應於粗糙表面,其長度爲位移。這種位移是存儲在置換貼圖。平滑的法線不須要存儲,由於它在渲染時是已知的(它只是一個插值的陰影法線)。黑矢量在紅矢量相交的點上是光滑的法線相交的無線細分粗網格細節表面。這些黑法線是存儲在法線貼圖裏。
置換貼圖圖和法線貼圖已經被預編譯並存儲在DDS文件。它們將被綁定到切線空間,因此這個模型能夠在實際使用時仍然能夠蒙皮和使用動畫,仍然使用相同的法線貼圖和位移貼圖。
因爲置換貼圖是關於限制特定細分曲面計算位移,置換貼圖應用於PN片細分不一樣於Catmull-Clark細分算法。在開發這個樣本的時候,咱們找不到一個工具,能夠在PN片細分中生成位移圖,因此咱們必須開發咱們本身的工具。
//*****************************************************************************************************
//* 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);
}
曲面細分階段,細分爲3個階段:外殼着色器(Hull - Shader)、Tessellation階段、域着色器階段(Domain - Shader )。一三階段可編程,第二階段不可編程。
圖7 細分流程
//*****************************************************************************************************
//*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
//*****************************************************************************************************
//* 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]
}
//*****************************************************************************************************
//* 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);
}