使用屏幕空間導數尋找三角形法線
使用生成的重心座標建立線框
線框寬度可配置 .html
made with Unity 2017.1.0.算法
網格由三角形組成,定義三角形是平的。使用表面法向量來增長表面彎曲的視覺效果,這使得建立光滑的表面網格成爲可能。然而,有時實際上想要顯示平坦的三角形,調試網格數據等等。數組
平面着色:爲了使三角形看起來像平坦的表面,必須使用三角形自帶的表面法線,而三角形表面法線又是經過三角形的三個頂點的法向量的平均獲得,表面法線使得網格具備多面外觀,稱爲平面着色。這也間接使得三角形之間不可能共享頂點,由於那樣它們會共享面法線,而共享面法線會使得多個三角處於同一平面不能達到完美的彎曲視覺效果。 所以,最終會獲得很是多的網格數據。 假如能夠共享頂點那就太好了,後面會探討共享面法線。app
線框:顯示網格的線框也可能有用,這使得網格的鏈接視覺效果更加直觀明顯。理想狀況下,咱們可使用自定義材質一次對任何網格進行平面着色和線框渲染。 編輯器
Shader "Custom/Flat Wireframe" { … }
因爲三角形屬於二維平面,所以在其平面上的每一個點的面法線都相同。 所以,渲染三角形內的每一個片斷都使用相同的法線向量。 三角形面法線向量怎麼計算? 在頂點程序vertex中,咱們只能訪問存儲在網格的頂點數據,除了美術設計初始定義此處存儲的向量就是法線向量用來表示三角形的法線,不然所有沒用。 在片斷程序fragment中,咱們只能訪問插值後的頂點法線。ide
計算三角形面法線向量:爲了肯定表面法線,咱們須要知道三角形在世界空間中的方向。這能夠經過三角形的三個頂點的位置來肯定。 假定它是nondegenerate Triangle非退化三角(不共線三點),則其法線向量等於三角形任意兩邊的叉積結果的歸一化值。 若是它是degenerate Triangle退化三角(),則不管如何都不會渲染。 所以,以逆時針方向給出三角形的頂點 a,b和c,其法線向量爲n =(c-a)x(b-a)。 經過歸一化,能夠獲得最終的單位法向矢量ˆn = n / | n |函數
三角形法線推導.this
實際上,咱們不須要使用三角形的頂點。只要位於三角形平面內的任意三個點就能夠。具體來講,咱們只須要位於三角形平面內的兩個向量,只要這兩個向量不平行且大於零便可。
有一種可使用上述討論的算法:渲染片斷時使用的世界位置座標。 例如,當前正在渲染的片斷在屏幕空間中的世界座標,就能夠獲得該片斷在右側的座標以及片斷在上方的座標。spa
若是咱們能夠訪問相鄰片斷的世界位置,那麼上述算法就能夠。雖然沒法直接訪問相鄰片斷的數據,可是咱們能夠訪問此數據的screen-space derivatives(屏幕空間導數詳細說明) 。 這是經過特殊指令完成的,該指令告訴咱們屏幕空間X或Y維度中任何數據片斷之間的變化率。
簡單解釋屏幕空間導數:例如,咱們當前片斷的世界位置爲P0,屏幕空間X維度的下一個片斷的位置是Px。 所以,這兩個片斷之間的X維度上的世界位置變化率是∂p / ∂x = Px – P0。 這是屏幕空間X維度中世界位置的偏導數。 咱們能夠經過內置的ddx函數在片斷程序fragment中獲取此數據,參數是提供頂點的世界座標。
void InitializeFragmentNormal(inout Interpolators i) { float3 dpdx = ddx(i.worldPos); … }
咱們能夠對屏幕空間的Y維度執行相同的操做,經過調用帶有世界位置的ddy函數來查∂p / ∂y = Py – P0
float3 dpdx = ddx(i.worldPos); float3 dpdy = ddy(i.worldPos);
由於這些值表示了片斷世界座標之間的差值,因此它們定義了三角形的兩條邊。咱們實際上不知道那個三角形的確切形狀,但它確定在原來三角形的平面上,這纔是最重要的。因此最終的法向量就是這些向量的標準化叉積。用這個向量覆蓋原來的法向量。
float3 dpdx = ddx(i.worldPos); float3 dpdy = ddy(i.worldPos); i.normal = normalize(cross(dpdy, dpdx));
ddx和ddy 是咋回事? 首先,有助於瞭解GPU着色老是一次評估2x2像素塊上的片斷/像素。 (即便最終僅須要繪製其中一些像素,而其餘像素
位於多邊形以外或被遮擋-不須要的片斷也會被mask掉,而不是寫入到緩衝區)。 着色器中變量(或表達式)v的屏幕空間導數是從2x2像素四邊形的一側到另外一側的v值(在代碼中的該點)的差。 即ddx
是右像素中v的值減去左像素中v的值,垂直方向上的ddy一樣。 這回答了「當咱們在屏幕上水平(ddx)或垂直(ddy)移動時,v增長或減小的速度有多快?」 -即用微積分的術語,它近
似於變量的偏導數(近似值,由於它在每一個片斷上使用離散樣本,而不是用數學方法評估函數的無窮小行爲)
對於標量,咱們也能夠把它當作一個梯度向量,∇v = float2(ddx(v), ddy(v)),他們指向屏幕空間中v增加最快的
方向。這種類型的信息一般在內部用於選擇紋理查找的適當的mipmap級別。對於大多數簡單的效果,你不須要使用這些導數,
由於基本的2D紋理採樣方法會爲你處理它。
建立一個新的材質,使用咱們的平面線框着色器。任何使用這種材質的網格都應該使用平面着色來渲染。它們看起來是多面的,雖然當你也使用法線貼圖時可能很難看到。在本教程的截圖中,我使用了標準的膠囊網格,使用灰色材質
從遠處看,膠囊看起來像是由四邊形組成的,但這些四邊形都是由兩個三角形組成的。
由三角形組成的四邊形
還有另外一種方法能夠肯定三角形的法線。咱們能夠使用實際的三角形頂點來計算法向量,而不是使用導數指令。這須要用在每一個三角形上,而不是每一個頂點或片斷上。這就是使用幾何着色器的地方。
幾何着色器階段位於頂點和片斷程序階段之間。它獲得頂點程序的輸出,按片元分組。在獲得插值後和用於渲染片斷以前,幾何程序階段能夠修改這些數據。
額外計算幾何着色階段的意義在於能夠給每一個元素都提供頂點,因此在咱們的例子中每一個三角形都有3個頂點。在這裏的三角形網格是否共享頂點並不重要,由於幾何程序會輸出新的頂點數據。這容許咱們得到三角形的法向量,並使用它做爲全部三個頂點的法向量。
添加幾何着色器的代碼文件[flatWireframe.cginc], 並定義一個MyGeometryProgram函數.
#if !defined(FLAT_WIREFRAME_INCLUDED) #define FLAT_WIREFRAME_INCLUDED #include "My Lighting.cginc" void MyGeometryProgram () {} #endif
注意:當shader model 4.0或更高時,幾何體着色器才支持。若是model目標被定義得很低時,Unity會自動增長到這個目標級別,可是低端手機能不能支持該model就得人爲調整。同時,要真正使用幾何着色器,咱們必須添加#pragma geometry指令,就像頂點和片斷函數同樣。最後,引用flatWireframe.cginc。將這些變化應用到平面着色器的basePass、additivePass和deferredPass中。
#pragma target 4.0 … #pragma vertex MyVertexProgram #pragma fragment MyFragmentProgram #pragma geometry MyGeometryProgram … //#include "My Lighting.cginc" #include "MyFlatWireframe.cginc"
定義輸出。回到編輯器將會獲得着色器編譯錯誤,由於咱們尚未正肯定義咱們的幾何函數。咱們必須聲明它會輸出多少頂點。這個數字能夠變化,因此咱們必須提供一個最大值。由於咱們使用的是三角形,因此每次調用老是輸出三個頂點。這是經過向函數中添加maxvertexcount屬性來指定的,參數爲3。
[maxvertexcount(3)]
void GeometryProgram () {}
定義輸入。頂點程序的輸出數據類型是插值後的頂點。因此在這種狀況下
[maxvertexcount(3)]
void MyGeometryProgram (InterpolatorsVertex i) {}
聲明語義。可是類型名稱在技術上是不正確的,那是咱們在命名它時沒有考慮到幾何着色器。在咱們的例子中是三角形。這必須在輸入類型以前指定語義。另外,因爲三角形每一個都有三個頂點,它們可構成一個數組,也要明確地定義它。
[maxvertexcount(3)]
void MyGeometryProgram (triangle InterpolatorsVertex i[3]) {}
由於幾何着色器能夠輸出的頂點數量是不一樣的,因此沒有一個單一的返回類型。相反,幾何着色器寫入到了片元流。在咱們的示例中,它是一個TriangleStream,必須將其指定爲inout參數。
[maxvertexcount(3)]
void MyGeometryProgram (
triangle InterpolatorsVertex i[3],
inout TriangleStream stream
) {}
TriangleStream相似於c#中的泛型類型。它須要知道頂點類型,也就是定義的struct InterpolatorsVertex。
[maxvertexcount(3)]
void MyGeometryProgram (
triangle InterpolatorsVertex i[3],
inout TriangleStream<InterpolatorsVertex> stream
) {}
如今函數定義徹底正確了,接下來必須將頂點數據放入流中。這是經過對每一個頂點調用流的Append函數來完成的,按照接收它們的順序放入。
[maxvertexcount(3)]
void MyGeometryProgram (
triangle InterpolatorsVertex i[3],
inout TriangleStream<InterpolatorsVertex> stream
) {
stream.Append(i[0]);
stream.Append(i[1]);
stream.Append(i[2]);
}
一個自定義的幾何程序階段配置完成
geometry program書寫很怪啊! Unity的着色器語法混合了CG和HLSL代碼。大多數狀況下它看起來像CG,但在這種狀況下它像HLSL。
基於1.2方法,開始計算每一個三角的頂點法線。
要找到三角形的法向量,首先要提取它的三個頂點的世界位置。
float3 p0 = i[0].worldPos.xyz; float3 p1 = i[1].worldPos.xyz; float3 p2 = i[2].worldPos.xyz; stream.Append(i[0]); stream.Append(i[1]); stream.Append(i[2]);
每一個三角形作一次標準化的叉乘
float3 p0 = i[0].worldPos.xyz; float3 p1 = i[1].worldPos.xyz; float3 p2 = i[2].worldPos.xyz; float3 triangleNormal = normalize(cross(p1 - p0, p2 - p0));
將頂點法線替換爲三角形法線.
float3 triangleNormal = normalize(cross(p1 - p0, p2 - p0));
i[0].normal = triangleNormal;
i[1].normal = triangleNormal;
i[2].normal = triangleNormal;
Flat shading, again.
上圖獲得了和之前同樣的結果,使用了幾何着色階段舞臺而不依賴於屏幕空間的派生指令。
哪一種方法最好? 若是你所須要的只是平面着色,那麼屏幕空間的漸變是實現這種效果最便宜的方法。而後你還能夠從網格數據中去除法線——Unity能夠自動作到這一點——也能夠移除法線插值數據。通常來講,若是你能夠不使用自定義幾何舞臺,那麼就這樣作。咱們將繼續使用幾何方法,由於咱們也須要它來進行線框渲染。
在處理完平面着色以後,咱們繼續渲染網格的線框。咱們不會建立新的幾何程序,也不會使用額外的pass來繪製線框。咱們將經過在三角形內部沿其邊緣添加線條效果來建立線框視覺效果。儘管定義形狀輪廓的線看起來只有內部線的一半粗,但足以建立一個使人信服的線框。
After taking care of the flat shading, we move on to rendering the mesh's wireframe. We're not going to create new geometry, nor will we use an extra pass to draw lines. We'll create the wireframe visuals by adding a line effect on the inside of triangles, along their edges. This can create a convincing wireframe, although the lines defining a shape's silhouette will appear half as thick as the lines on the inside. This usually isn't very noticeable, so we'll accept this inconsistency.
要向三角形邊緣添加線框效果,咱們須要知道片斷到最近邊緣的距離。這意味着關於三角形的信息須要在片斷程序中可用。這能夠經過向內插數據中添加三角形的質心座標來實現。
重心座標計算
由於網格數據不提供重心座標,因此頂點程序不知道。爲了讓幾何程序輸出它們,咱們必須定義一個新的結構。它應該包含與內插頂點相同的數據
struct InterpolatorsGeometry {
InterpolatorsVertex data;
};
調整流數據類型,使其使用新的結構。
void MyGeometryProgram (
triangle InterpolatorsVertex i[3],
inout TriangleStream<InterpolatorsGeometry> stream
) {
…
InterpolatorsGeometry g0, g1, g2;
g0.data = i[0];
g1.data = i[1];
g2.data = i[2];
stream.Append(g0);
stream.Append(g1);
stream.Append(g2);
}
添加額外的數據到插值幾何,增長TEXCOORD9類型重心座標變量.
struct InterpolatorsGeometry { InterpolatorsVertex data; float3 barycentricCoordinates : TEXCOORD9; };
給每一個頂點分配一個質心座標。
g0.barycentricCoordinates = float3(1, 0, 0); g1.barycentricCoordinates = float3(0, 1, 0); g2.barycentricCoordinates = float3(0, 0, 1); stream.Append(g0); stream.Append(g1); stream.Append(g2);
注意,質心座標的總和老是1。只須要傳遞其中兩個,經過從中減去兩個座標來獲得第三個座標。
struct InterpolatorsGeometry { InterpolatorsVertex data; float2 barycentricCoordinates : TEXCOORD9; }; [maxvertexcount(3)] void MyGeometryProgram ( triangle InterpolatorsVertex i[3], inout TriangleStream<InterpolatorsGeometry> stream ) { … g0.barycentricCoordinates = float2(1, 0); g1.barycentricCoordinates = float2(0, 1); g2.barycentricCoordinates = float2(0, 0); … }
將重心座標傳遞給片斷程序,但不能簡單地使用這些數據,需在My Lighting.cginc文件定義宏:CUSTOM_GEOMETRY_INTERPOLATORS,來肯定是否可以使用。
struct Interpolators { … #if defined (CUSTOM_GEOMETRY_INTERPOLATORS) CUSTOM_GEOMETRY_INTERPOLATORS #endif };
如今咱們能夠在MyFlatWireframe中定義這個宏。咱們必須在引入My Lighting以前作這個。咱們也能夠在插值幾何中使用它,因此咱們只須要寫一次代碼
#define CUSTOM_GEOMETRY_INTERPOLATORS \ float2 barycentricCoordinates : TEXCOORD9; #include "My Lighting.cginc" struct InterpolatorsGeometry { InterpolatorsVertex data; // float2 barycentricCoordinates : TEXCOORD9; CUSTOM_GEOMETRY_INTERPOLATORS };
咱們如何使用重心座標來可視化線框?也要照明參數不該參與計算。
建立新文件myLightingInput.cginc
#if !defined(MY_LIGHTING_INPUT_INCLUDED) #define MY_LIGHTING_INPUT_INCLUDED #include "UnityPBSLighting.cginc" #include "AutoLight.cginc" #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) #if !defined(FOG_DISTANCE) #define FOG_DEPTH 1 #endif #define FOG_ON 1 #endif … float3 GetEmission (Interpolators i) { #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS) #if defined(_EMISSION_MAP) return tex2D(_EmissionMap, i.uv.xy) * _Emission; #else return _Emission; #endif #else return 0; #endif } #endif
在myLighting.cginc刪除重複代碼
#if !defined(MY_LIGHTING_INCLUDED) #define MY_LIGHTING_INCLUDED //#include "UnityPBSLighting.cginc" // … // //float3 GetEmission (Interpolators i) { // … //} #include "My Lighting Input.cginc" void ComputeVertexLightColor (inout InterpolatorsVertex i) { #if defined(VERTEXLIGHT_ON) i.vertexLightColor = Shade4PointLights( unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, i.worldPos.xyz, i.normal ); #endif }
如今就能夠在線框shaderMyFlatWireframe.分開計算了
#include "My Lighting Input.cginc" #include "My Lighting.cginc"
定義線框專屬宏ALBEDO_FUNCTION
.
#include "My Lighting Input.cginc" #if !defined(ALBEDO_FUNCTION) #define ALBEDO_FUNCTION GetAlbedo #endif
用宏指令ALBEDO_FUNCTION
.替換掉GetAlbedo
函數.
float3 albedo = DiffuseAndSpecularFromMetallic
(
ALBEDO_FUNCTION(i), GetMetallic(i), specularTint, oneMinusReflectivity
);
在線框shader增長線框函數GetAlbedoWithWireframe , 它首先要計算一次原始的albedo,而後再計算線框!
#include "My Lighting Input.cginc" float3 GetAlbedoWithWireframe (Interpolators i) { float3 albedo = GetAlbedo(i); return albedo; } #define ALBEDO_FUNCTION GetAlbedoWithWireframe #include "My Lighting.cginc"
直接使用重心座標做爲反照率.
float3 GetAlbedoWithWireframe (Interpolators i) { float3 albedo = GetAlbedo(i); float3 barys; barys.xy = i.barycentricCoordinates; barys.z = 1 - barys.x - barys.y; albedo = barys; return albedo; }
重心座標as albedo
爲了建立線框效果,須要知道片斷離最近的三角形邊緣有多近。咱們能夠經過取重心座標的最小值來獲得它,在重心區域內獲得了到邊緣的最小距離,咱們直接用它來表示反照率。
float3 albedo = GetAlbedo(i); float3 barys; barys.xy = i.barycentricCoordinates; barys.z = 1 - barys.x - barys.y; // albedo = barys; float minBary = min(barys.x, min(barys.y, barys.z)); return albedo * minBary;
最小距離.
統一以最近距離會致使粗細不一致,須要使用smoothstep
過渡
float minBary = min(barys.x, min(barys.y, barys.z)); minBary = smoothstep(0, 0.1, minBary); return albedo * minBary;
調整後的過渡
上述線框效果只適用於邊長大體相同的三角形,同時有遠近視角緣故,線有粗有細。兩個方向的屏幕空間導數值可能有負有正,取它們的絕對值
float minBary = min(barys.x, min(barys.y, barys.z)); float delta = abs(ddx(minBary)) + abs(ddy(minBary)); minBary = smoothstep(0, delta, minBary);
fwidth
函數也能夠表示上述兩段代碼!
//float minBary = min(barys.x, min(barys.y, barys.z)); //float delta = abs(ddx(minBary)) + abs(ddy(minBary)); float delta = fwidth(minBary);
固定寬度線框
若是以爲產生的線顯得有點細,能夠經過將過渡從邊緣偏移一點來解決粗度,例如用與混合範圍相同的值。
minBary = smoothstep(delta, 2 * delta, minBary);
增厚的線框(有鋸齒).
產生更粗更清晰的線條,但也會在三角形附近的線條中顯示鋸齒。這些鋸齒影的出現是因爲這些區域最近的邊緣過渡太忽然,不連續的致使的。爲了解決這個問題,咱們必須先計算重心座標的導數,再混合,而後在那以後獲取最小值.
/*先取出一次最近距離,先計算偏導取絕對值算出過渡*/ barys.z = 1 - barys.x - barys.y; //float3 deltas = fwidth(barys); float minBary = min(barys.x, min(barys.y, barys.z)); float delta = abs(ddx(minBary)) + abs(ddy(minBary)); /*用過渡增長重心距離*/ barys = smoothstep(deltas, 2 * deltas, barys); /*再取一次最小距離*/ float minBary = min(barys.x, min(barys.y, barys.z)); //float delta = fwidth(minBary); //minBary = smoothstep(delta, 2 * delta, minBary); return albedo * minBary;
高級線框.
線框效果有了,但有可能須要使用其餘線寬、混合顏色,也許想對每種材料使用不一樣的設置。 所以,向着色器添加三個屬性。
首先是線框顏色,其次是線框平滑度,控制過渡範圍。 從0到10的範圍應該足夠,默認值爲1,表明寬度測量的倍數。 第三是線框厚度,其設置與平滑相同。
_WireframeColor ("Wireframe Color", Color) = (0, 0, 0) _WireframeSmoothing ("Wireframe Smoothing", Range(0, 10)) = 1 _WireframeThickness ("Wireframe Thickness", Range(0, 10)) = 1 ... float3 _WireframeColor; float _WireframeSmoothing; float _WireframeThickness; float3 GetAlbedoWithWireframe (Interpolators i) { float3 albedo = GetAlbedo(i); float3 barys; barys.xy = i.barycentricCoordinates; barys.z = 1 - barys.x - barys.y; float3 deltas = fwidth(barys); float3 smoothing = deltas * _WireframeSmoothing; float3 thickness = deltas * _WireframeThickness; barys = smoothstep(thickness, thickness + smoothing, barys); float minBary = min(barys.x, min(barys.y, barys.z)); // return albedo * minBary; return lerp(_WireframeColor, albedo, minBary); }
建立新的屬性自定義着色器GUI。
void DoWireframe () { GUILayout.Label("Wireframe", EditorStyles.boldLabel); EditorGUI.indentLevel += 2; editor.ShaderProperty( FindProperty("_WireframeColor"), MakeLabel("Color") ); editor.ShaderProperty( FindProperty("_WireframeSmoothing"), MakeLabel("Smoothing", "In screen space.") ); editor.ShaderProperty( FindProperty("_WireframeThickness"), MakeLabel("Thickness", "In screen space.") ); EditorGUI.indentLevel -= 2; }
public override void OnGUI ( MaterialEditor editor, MaterialProperty[] properties ) { this.target = editor.target as Material; this.editor = editor; this.properties = properties; DoRenderingMode(); if (target.HasProperty("_WireframeColor")) { DoWireframe(); } DoMain(); DoSecondary(); DoAdvanced(); }
可配置線框屬性.