遊戲中點乘的運用

原文:

http://www.manew.com/forum.php?mod=viewthread&tid=107084&page=1#pid1568321

https://www.sohu.com/a/167651842_667928

 

另外幾篇叉乘和點乘的

https://zhuanlan.zhihu.com/p/89046275

https://blog.csdn.net/qiaoquan3/article/details/70194685

 

最近蠻牛乾貨發表了很多關於Shader的帖子,介於大環境這麼好,我也投入到了Shader中的學習中,在學習中發現一些問題,往往標準的模型映射MVP之流反而沒有什麼難度,實際上他們到像是過場跑龍套的。進過學習分析,每個shader的原理一般只是有一個核心概念或者核心公式,其中而關於Dot點乘的應用部分,往往讓我不能迅速理解,所以這裏分析三篇Shader帖子中的關於Dot點乘的應用,徹底掀開她的蓋頭來。

 

光照中的點乘應用

本節效果

原文Shader詳解 ,http://gad.qq.com/program/translateview/7173932

漫反色中的核心函數

fixed4 diff = albedo * _LightColor0 * max(0, dot(i.worldNormal, worldLight));

當 worldLight 爲float3(0, 1, 0),時,主要分析該函數 dot(float3(0, 1, 0), i.normal);

點乘兩個向量,我們知道向量歸一化以後向量點乘的值是向量的夾角的Cos值(簡單理解)

根據圖片我們知道,如果兩個向量的夾角爲0,也就是向量方向相同,那麼cos這個值趨近於1。

這裏需要注意的一點就是光源的方向是指指向光源的方向向量,而不是光的方向向量。

我們知道指向光源的方向和法向量的夾角不會大於90度,如果出現大於90度說明光源在平面的背面,我們不需要負光的出現,所以需要“受鉗制的光照”,這裏有兩個函數可以選擇

一個是:max(0, dot(float3(0, 1, 0), i.normal)); 這個函數比較簡單,取最大值,由於cos的最大值是1,所以產生的值是(0,1)

另一個是:saturate(dot(float3(0, 1, 0), i.normal));這個函數內置的飽和值函數,當然這個函數把結果限制在0和1之間

最後一個是高級函數, UnityStandardBRDF導入文件定義了方便的DotClaped函數(實際是根據顯卡做了性能選擇和封裝,詳情見本節鏈接)

另外說一個關於反色光的計算問題,也就是我們初中必背的一個公式,入射角=反色角,這裏shaderlab提供了一個內置函數 reflect

float3 reflectionDir = reflect(-lightDir, i.normal);

這裏需要注意的這個函數負的 –lightDir 因爲這裏的參數需要的是光源的方向,且兩個參數需要歸一化的,具體的延伸閱讀見下圖

 

邊緣提取中的點乘應用

本節效果1 輪廓增強

出處 http://www.manew.com/thread-102911-1-1.html [zhang273162308] Unity&Shader基礎篇—輪廓增強

本節效果2 邊緣自發光

出處2 http://blog.csdn.net/jk823394954/article/details/48983621 Unity Shader 表面着色器邊緣光(Rim Lighting)二

效果1分析:

獲取邊緣的核心函數如下,又看到了我們提到的向量點乘

float newOpacity = min(1.0, tex.a / abs(dot(viewDirection, normalDirection)));

return float4(col, newOpacity);

 

這裏我們先看下 viewDirection是怎麼求得

output.viewDir = normalize(_WorldSpaceCameraPos- mul(modelMatrix, input.vertex).xyz);

根據向量的減法我們知道兩個向量相減得到的向量的方向由減數指向被減數,如下圖所示:

所以我們 這個viewDir就是世界座標系中模型指向相機的,和上一節的光照方向是一樣的。這樣我們在分析下邊緣提取的公式。

min(1.0, tex.a / abs(dot(viewDirection, normalDirection)));

1、dot(viewDirection, normalDirection),這個我想大家都明白就是求相機和法向量的餘弦值,夾角越小值越趨近1,夾角越大值越趨近於0,也就是值大小與夾角成反比

2、abs(dot(viewDirection, normalDirection)),求絕對值,我們知道當夾角大於90度的時候,cos爲負值,也就是攝像頭從背面看物體(你以爲是透視啊,實際shader有這個能力的)所以這裏爲什麼用的abs而不是上節中的max(有待證明)

3、tex.a / abs(dot(viewDirection, normalDirection)),首先求反,也就是1/x,我們在1中知道dot的值與夾角成反比,再求反的話,負負爲正了,該值變成與夾角成正比了,由於又做了絕對值操作,所以求反的值也在(0,1)之間但是趨近於1的時候,說明法線和視線垂直(正交),也就是邊緣點。

4、tex.a*這個(0,1)的值,也就是說夾角越大(在邊緣)透明度越高,在中心透明度越低,這就自然形成邊緣高亮或者輪廓高亮的效果了

5、min(1.0, tex.a / abs(dot(viewDirection, normalDirection)));,由於顏色的值是在(0,1)之間的大於1也沒有意義,所以可以用這個min函數限制最大值是1

效果分析2:

核心公式

half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal)); 

//賦值自發光顏色信息

o.Emission = _RimColor.rgb * pow (rim, _RimPower); 

1、dot (normalize(IN.viewDir), o.Normal)和效果1一樣,也就是視線與法線夾角成反比,而我們知道夾角正交(垂直)的地方是邊緣,那麼1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));  相當於再次求反,負負爲正,那麼就變成夾角越大值越大,也就是越是邊緣越趨近1,自然實現了自發光效果了。

積雪效果的點乘綜合應用--積雪實現

效果圖

詳細實現請參考 http://www.cnblogs.com/polobymulberry/p/4316683.html 

【譯】Unity3D Shader 新手教程(2/6) —— 積雪Shader

上代碼

  1. void surf (Input IN, inout SurfaceOutput o) {
  2. //該像素的真實顏色值
  3. half4 c = tex2D (_MainTex, IN.uv_MainTex);
  4. //從凹凸貼圖中得到該像素的法向量
  5. o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));
  6. //得到世界座標系下的真正法向量(而非凹凸貼圖產生的法向量)和雪落
  7. //下相反方向的點乘結果,即兩者餘弦值,並和_Snow(積雪程度)比較
  8. if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow))
  9. //此處我們可以看出_Snow參數只是一個插值項,當上述夾角餘弦值大於
  10. //lerp(1,-1,_Snow)=1-2*_Snow時,即表示此處積雪覆蓋,所以此值越大,
  11. //積雪程度程度越大。此時給覆蓋積雪的區域填充雪的顏色
  12. o.Albedo = _SnowColor.rgb;
  13. else
  14. //否則使用物體原先顏色,表示未覆蓋積雪
  15. o.Albedo = c.rgb;
  16. o.Alpha = 1;
  17. }

效果很美,代碼註釋很好,還記的本文的背景嘛,每個shader都有它的靈魂和核心公式,這篇翻譯的也不錯,但是美中不足,核心公式可讀性差了些。我們來分析下,首先落雪的原理是什麼,現實中能積雪的是什麼地方呢?是一個平臺要有面積才能承接上積雪,當然面的方向要與下雪的方向垂直,很少見到牆面上能積雪的一般都是屋頂是這樣吧。結合前兩節的關於小芝麻Dot的學習,我們知道通過Dot我們可以求得面法線與指定方向的夾角,從而求得面與方向的夾角和邊緣位置,這裏我們可以把落雪的方向當成光源的方向實際上和漫反色一個道理,“落雪方向的反方向和麪法向量夾角越小,說明落雪方向與面垂直,積雪越多“,下面我們看下shader的核心公式,我們分解分析下

if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow))

1、dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) 翻看_SnowDirection知道是傳入變量也確實是落雪反方向(模型指向落雪方向),那麼這個dot就是求得法線與該方向的cos值,角度越小值越大,也就是面越垂直值越小,下文中的”落雪方向“都指的是實際落雪的反方向

2、lerp(1,-1,_Snow)是什麼1,-1的插值函數,惡補下公式

float lerp(float a, float b, float w) {   return a + w*(b-a);
}
也就是註釋給的1-2*_Snow

//此處我們可以看出_Snow參數只是一個插值項,當上述夾角餘弦值大於

//lerp(1,-1,_Snow)=1-2*_Snow時,即表示此處積雪覆蓋,所以此值越大,

//積雪程度程度越大。此時給覆蓋積雪的區域填充雪的顏色

看到這個計算式子,其實是一個斜率爲負值的直線方程,也就是該值與_Snow參數 成反比,值越大結果值越小(讀到這裏估計很多人一頭霧水了)

3、dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow)

按照註釋大家理解下這個公式,感覺如何,我第一次度的時候覺得挺繞的,後來仔細想想,問題出在原作者的編碼風格上,這裏實際上代碼規範做的不好,沒有遵循單一原則。下面我來分解下

A、不等式左邊,dot部分,法線與”落雪方向“夾角越小值越大,符合我們分析的原理,面與”落雪方向“垂直值越大,很好理解,這裏與兩個向量的夾角成反比,但是面與指定方向成正比,也就是面和落雪方向夾角越大,值越大,正是我們需要的;

B、不等式右邊,lerp部分,是什麼?不是什麼插值計算,最後求得,是一個閾值是一個常量。

那不等式整體的意思是什麼呢,當法線與”落雪方向“夾角小與一個值是 就會積雪,反過來好理解當面與落雪方向夾角大於一個值就落雪,好理解吧。

那B中的lerp部分中是什麼呢,其實是另一件事情,它是求這個閾值的,當參數越大返回值越小,而閾值越小則允許落雪的角度差越大就是就會落更多的雪,其實也是一個求反,以符合人的慣性思維_Snow越大代表雪越多,恰好不等式右邊越小雪量越大,是不是分開更好理解,一行代碼只做一件事情。

4、不完美的地方

根據源碼分析,我們發現dot沒有做負值判斷,也就是當發現視線和”落雪方向“大於90度時,會出現負值,實際上這種情況根本就是多餘的,物體的內面根本不會出現積雪,至於插值 lerp(1,-1,_Snow)的下降速度採用1,0也應該是沒有問題的。

積雪效果的點乘綜合應用--積雪厚度變形

上代碼

  1. void vert (inout appdata_full v) {
  2. //將_SnowDirection轉化到模型的局部座標系下
  3. float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
  4. if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3))
  5. {
  6. v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;
  7. }
  8. }

這裏我引下原文的原理說明,很簡單了

首先我們傳給vert函數一個參數appdata_full v,參數的類型爲appdata_full(Unity內置類型),該類型包含了紋理座標,法向量,頂點位置,以及切線信息。如果你還需要使用其他的數據類型,你可以使用自定義的輸入結構體作爲pixel函數的第二個參數傳遞額外的信息 — 目前我們不需要這樣做。

_SnowDirection使用的是世界座標系,但是我們需要的其實是模型局部座標系下的_SnowDirection。所以我們需要先將_SnowDirection轉化到模型的局部座標系下。而我們只需要將_SnowDirection乘以Unity內置矩陣 – UNITY_MATRIX_IT_MV(IT表示Inverse Transpose逆轉置矩陣,MV表示 ModelView矩陣,該矩陣表示是ModelView的逆轉置矩陣)。

現在我們得到了該頂點的法向量(vert函數應該是對每個vertex調用一次,相對於surf函數對每個pixel調用一次)。我們仍然像上面做積雪效果時那樣,將轉換座標系空間後的雪落下相反方向和模型局部座標系下的法向量進行點乘,得到的結果仍然和一個插值比較。不過此時插值項不再是_Snow,而是_Snow*2/3,這表示只有那些接近雪落下方向的區域纔會增加雪的厚度,更符合自然現象。

而這些通過測試的區域,沿着(sn.xyz+v.normal)方向進行加厚,也就是將其頂點沿此方向伸展一定距離。注意到增厚的程度取決於_SnowDepth和_Snow,而增厚的方向是由物體法向和雪落的方向綜合作用的,這也符合自然現象。

原理用一句話概括,就是通過dot與閾值判斷,加大面頂點的高度,以獲得加厚的效果,注意這裏應用的向量的加法。

 

總結

本篇是本人學習shader中dot應用的一篇總結貼,我自己總結的一句話每個shader中都有一個核心原理和公式,關於dot的應用無非是求邊緣和求非邊緣?

 

 

 

 

 

 

 

 

 

 

 

 

 

在上一篇Unity3d Shader中的小芝麻(Dot點乘解惑篇)的文尾巴,我留了一個引子,“關於dot的應用無非是求邊緣和求非邊緣?”,

確實Dot在shader中的應用主要是求邊緣或者非邊緣(也就是面),但是並不是全部,本文的目的是結合向量點積的定義,收集和總結向量點乘Dot(點積)在Unity(game)中的應用。下面看一下向量點積的定義

不用說太多的數學知識,我們把定義分爲這三種吧,1、幾何意義,2、代數意義,3、矩陣乘法(向量投影)相關。下面我們就用實際中的例子來繼續解釋這三種定義在遊戲開發中的應用。

實現

a.b = x1x2+y1y2

代數意義,實際上如果數學一般的同學,看到這個數學式子,應該是沒有什麼感覺的我也是如此,後來在不斷學習中發現,這麼小的一個式子應用竟然如此廣泛

A、計算向量長度和平方(模的平方)

如果說你對a.b = x1x2+y1y2 不敏感的話,如果dot中的參數相同也就是 a.a = x1x1+y1y2(勾股定理?),是的向量長度的平方,這個的一個應用是在點光源的衰減因子計算上(與到光源的距離平方成反比)

B、實現灰度公式的快速計算

計算灰度在遊戲中是一種常用的圖像操作,比如人物死後的畫面,不能點擊的按鈕,死人的頭像等等。求灰度的公式是什麼呢?

對於彩色轉灰度,有一個很著名的心理學公式:

Gray = R*0.299 + G*0.587 + B*0.114

看到這個式子,可能大家都笑了,這次我們對於算式開始敏感了,這就不是點積嗎?我們看下例子中的Shader代碼

效果如下:

代碼很簡單,這裏就不詳細說了

補充實際上灰度化也可以使用顏色平均值,這種簡便高效的方式

幾何意義,實際上一篇中應用的也是幾何意義,求邊緣的依據也是歸一化向量夾角的cos值

A、Dot正負值應用,判斷主角與目標的前後位置,

我們補充一個常用的dot幾何意義的補充,根據Dot值,獲得目標的位置信息,通過正負值,判斷目標前後位置(負值在求邊緣中被我們捨棄的部分),文字不好描述上圖和代碼很清晰了

判斷敵我方位

1.判斷目標在自己的前後方位可以使用下面的方法:

Vector3.Dot(transform.forward, target.position-transform.position)

返回值爲正時,目標在自己的前方,反之在自己的後方

2.判斷目標在機子的左右方位可以使用下面的方法:

Vector3.Cross(transform.forward, target.position-transform.position).y

返回值爲正時,目標在自己的右方,反之在自己的左方

補充計算目標左右方的代碼參考一下代碼

public static bool RotationDirection(Vector3 currentDir, Vector3 previousDir) {

if (Vector3.Cross(currentDir, previousDir).z > 0) {

//順時針 return true; } else {

//逆時針 return false;

}

}

作者:影子丟了

鏈接:http://www.jianshu.com/p/6bd8623a825e

來源:簡書

B、求向量夾角

我想通過Dot幾何意義的求向量夾角,可能是我們最熟悉的了,具體就是應用高中的反餘弦函數大家都懂

void OnGUI ()

{

//點積的返回值

float c = Vector3.Dot (a, b);

//向量a,b的夾角,得到的值爲弧度,我們將其轉換爲角度,便於查看!

float angle = Mathf.Acos (Vector3.Dot (a.normalized, b.normalized)) * Mathf.Rad2Deg;

GUILayout.Label ("向量a,b的點積爲:" + c);

GUILayout.Label ("向量a,b的夾角爲:" + angle);

//叉積的返回值

Vector3 e = Vector3.Cross (a, b);

Vector3 d = Vector3.Cross (b, a);

//向量a,b的夾角,得到的值爲弧度,我們將其轉換爲角度,便於查看!

angle = Mathf.Asin (Vector3.Distance (Vector3.zero, Vector3.Cross (a.normalized, b.normalized))) * Mathf.Rad2Deg;

GUILayout.Label ("向量axb爲:" + e);

GUILayout.Label ("向量bxa爲:" + d);

GUILayout.Label ("向量a,b的夾角爲:" + angle);

}

矩陣乘法相關(向量投影)

實際也代數意義相關,在向量的旋轉計算中我們引入了旋轉矩陣,根據背景中列出的3部分,我們可以用點積來進行矩陣運算的式子分解

如算式敏感的同學的旋轉矩陣的應用

這裏我們反的看(c是cos,sin是sin)

uv = float2(uv.x*c - uv.y*s, uv.x*s + uv.y*c);

尼瑪,我算式敏感了,這是什麼?這不是點積嘛,然後我們得出

uv = float2(dot(uv, float2(c, -s)), dot(uv, float2(s, c)));

我的數學天賦有上升了一個層次根據“a·b=a^T*b,這裏的a^T指示矩陣a的轉置”然後我們得出了

漂亮的效果圖

關於如何通過點積獲得的旋轉,也就是

uv = float2(dot(uv, float2(c, -s)), dot(uv, float2(s, c)));

總結

以上兩篇文中提到知識點,是我關於Dot向量點乘的在圖形圖像(game)應用的總結,以我羸弱的數學知識來說實屬不易,最後慨嘆一下點積 a.b=|a||b|cos(a,b)這個小小的數學公式竟然有這麼大的威力!希望以上知識對你有用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章