一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

Signed Distance Field Shadow in Unity

 睡神在在 2018-08-30

0x00 前言

最近讀到了一個今年GDC上很棒的分享,是Sebastian Aaltonen帶來的利用Ray-tracing實現(xiàn)一些有趣的效果的分享。
WechatIMG111.jpeg
其中有一段他介紹到了對Signed Distance Field Shadow的改進(jìn),主要體現(xiàn)在消除SDF陰影的一些artifact上。
image.png
第一次看到Signed Distance Field Shadow是在大神Inigo Quilez的博客上,較傳統(tǒng)的陰影實現(xiàn)方式,例如shadow map,視覺效果要好很多??梢钥吹较聢D中物體的陰影隨著距離由近到遠(yuǎn)也逐漸由清晰漸漸過渡到模糊的效果,表現(xiàn)更加自然而真實。
image.png
相比較而言,Unity中的陰影實現(xiàn)效果就簡單并且死板了許多。
屏幕快照 2018-06-10 下午6.32.40.png

下面我們就在Unity中來實現(xiàn)RayMarching,并利用SDF繪制一些簡單的物體,最后實現(xiàn)一下陰影的效果。

0x01 在Unity中實現(xiàn)SDF

首先,RayMarching算法處理的是屏幕上的每一個像素,因此在Unity中我們自然而然會想到利用屏幕后處理的方式來實現(xiàn)RayMarching。
所以,RayMarching的主要邏輯都在Fragment Shader內(nèi)實現(xiàn),而Vertex Shader則主要用來獲取頂點屬性中所保存的射線信息,之后經(jīng)過插值傳入Fragment Shader中,供每一個Fragment來使用。此時整個屏幕是一個四邊形,一共有4個頂點,這4個頂點就可以用來記錄屏幕上的4根射線,而這4根射線的方向就可以直接取攝像機(jī)的平截頭體的4條邊的方向,之后再經(jīng)過插值生成射向某個片元的射線。
?1528627667019.jpg

這里我們可以直接調(diào)用Unity提供的Camera.CalculateFrustumCorners方法,這里是相關(guān)文檔(https://docs./ScriptReference/Camera.CalculateFrustumCorners.html)。
下面是這個方法的簽名:

public void CalculateFrustumCorners(Rect viewport, float z, 
              Camera.MonoOrStereoscopicEye eye, Vector3[] outCorners);

其中作為我們需要的4個outCorners也是作為參數(shù)傳入這個方法的。不過需要注意的是該方法獲取的平截頭體的4條邊是在local space的,所以我們需要將它們轉(zhuǎn)移到world space,以供Fragment Shader中使用。
這樣我們就得到了4個向量,但是這4個向量要怎么向Shader中傳遞效率才高呢?如果每一個向量傳遞一次,則效率并不高。所以這里我們使用一個矩陣來保存這4個向量,而向shader中傳送數(shù)據(jù)就只需要傳送一個矩陣。

    Transform camtr = cam.transform;
    Vector3[] frustumCorners = new Vector3[4];
    cam.CalculateFrustumCorners(new Rect(0, 0, 1, 1), 
        cam.farClipPlane, cam.stereoActiveEye, frustumCorners);
    var bottomLeft = camtr.TransformVector(frustumCorners[0]);
    var topLeft = camtr.TransformVector(frustumCorners[1]);
    var topRight = camtr.TransformVector(frustumCorners[2]);
    var bottomRight = camtr.TransformVector(frustumCorners[3]);

    Matrix4x4 frustumCornersArray = Matrix4x4.identity;
    frustumCornersArray.SetRow(0, bottomLeft);
    frustumCornersArray.SetRow(1, bottomRight);
    frustumCornersArray.SetRow(2, topLeft);
    frustumCornersArray.SetRow(3, topRight);
    return frustumCornersArray;

射線的數(shù)據(jù)準(zhǔn)備好了,向shader中傳送數(shù)據(jù)在Unity中也十分簡單,只需要調(diào)用SetMatrix就好。但是這里又出現(xiàn)了一個新的問題,那就是shader如何正確的確定它所處理的是哪根射線呢?如果不能確定頂點所對應(yīng)的射線,那么之后的插值結(jié)果就不會正確。所以在Vertex Shader中我們需要一個Index來從傳入的矩陣中正確的取出射線方向。
那么Index要如何確定呢?
聰明的你一定想到了,對一個四邊形來說,它的UV數(shù)據(jù)是很有規(guī)律的。所以我們就可以在Vertex Shader中利用UV數(shù)據(jù)來確定正確的射線:

    index = v.uv.x + (2 * o.uv.y);
    o.ray = _Corners[index].xyz;

OK,之后只要在Fragment Shader中使用經(jīng)過插值的ray數(shù)據(jù),就能獲取當(dāng)前Fragment所對應(yīng)的射線方向了。到此,我們已經(jīng)將射線引入了Shader中。

接下來我們來定義一個SDF,使用SDF來定義我們將要渲染的內(nèi)容。我們可以在Inigo Quilez的博客上獲取很多常見物體的SDF定義,鏈接在這里:(http://.org/www/articles/distfunctions/distfunctions.htm)。
下面我們就在Unity中利用SDF渲染一個六棱體:

float sdHexPrism( float3 p, float2 h )
{
    float3 q = abs(p);
    return max(q.z-h.y,max((q.x*0.866025+q.y*0.5),q.y)-h.x);
}

針對不同的物體定義都需要一個SDF來描述該物體,但是如果在我們的RayMarching算法中每次想要渲染不同的形狀時都要修改一下SDF的話似乎十分不方便,所以通常我們還會定義一個更高層的抽象——也可以叫做SDF函數(shù)——這個函數(shù)常常被稱作map,它的輸入是一個點坐標(biāo),輸出則是該點距離SDF所定義的物體表面的最近距離。
而有了map這個高層的抽象,我們可以很方便的在map的內(nèi)部實現(xiàn)中按照自己的需求修改SDF,例如將一些基礎(chǔ)的物體進(jìn)行合并、拆分等等。從這個角度講,map其實定義了我們要渲染的整改場景,因此正個場景的信息我們是已知的,這一點在之后渲染陰影的時候會用到。
不過,我們還是先來看一個簡單的例子,下面就是我們畫六棱體的例子中所使用的map的定義:

        float map(float3 rp)
        {
            float ret = sdHexPrism(rp, float2(4, 5));

            return ret;
        }

之后我們在Fragment Shader中實現(xiàn)該Fragment上的RayMarching邏輯,在引入SDF之后,RayMarching的每一次Marching的距離就可以根據(jù)SDF的結(jié)果來設(shè)定了,我想大家應(yīng)該都見過類似這樣的圖解:

ref:adrian's soapbox

可以看到,每一次marching的距離就是當(dāng)前采樣點到SDF定義的表面的最近距離,直到采樣點和表面重合,即光線和表面相交了。
所以我們只需要在Fragment Shader中跑一個for循環(huán),每一次迭代都調(diào)用一次map來確認(rèn)當(dāng)前采樣點距離SDF的最近距離surfaceDistance,如果surfaceDistance不為0,則下一次marching的距離就是surfaceDistance;如果為0,則證明光線和表面相交,我們只需要確定這點的顏色就好了。
除此之外,我們需要相機(jī)的位置rayOrigin做為射線的起點,這個值我們可以通過在腳本中調(diào)用SetVector將相機(jī)的位置傳給GPU。此外我們還需要該Fragment上的射線方向rayDirection,我們可以直接獲取,因為它就是頂點屬性中的ray經(jīng)過插值之后的結(jié)果。

所以這是一個很簡單的邏輯:

        fixed4 raymarching(float3 rayOrigin, float3 rayDirection)
        {

            fixed4 ret = fixed4(0, 0, 0, 0);

            int maxStep = 64;

            float rayDistance = 0;

            for(int i = 0; i < maxStep; I++)
            {
                float3 p = rayOrigin + rayDirection * rayDistance;
                float surfaceDistance = map(p);
                if(surfaceDistance < 0.001)
                {
                    ret = fixed4(1, 0, 0, 1);
                    break;
                }

                rayDistance += surfaceDistance;
            }
            return ret;
        }

OK,光線和表面相交之后,輸出一個紅色。
我們來看一下實際的結(jié)果:
屏幕快照 2018-06-11 下午3.44.55.png
可以看到,場景的Hierachy中空空如也,但是屏幕上卻出現(xiàn)了一個純色的六棱體。

0x02 梯度、法線和光照

當(dāng)然,這個效果并不吸引人,因此我們顯然要加入一些光照效果來提升表現(xiàn)力。那么求表面的法線就是必須要做的一件事情了。
milo的《用 C 語言畫光(四):反射 》這篇文章中也有相關(guān)的內(nèi)容,即距離場變化最大的方向便是法線方向。根據(jù)矢量微積分(vector calculus),一個純量場(scalar field)的最大變化方向就是其梯度(gradient),所以這個問題就轉(zhuǎn)化為求形狀邊界位置的 SDF 梯度——即求各個方向的變化率,也就是要求導(dǎo)了。
不過我們顯然沒有必要真正的計算求導(dǎo),只需要找一個能夠得到近似效果的方式就好了。我們常常使用這個下面這個算式來近似SDF梯度,即在這一點的表面法線:
屏幕快照 2018-06-11 下午5.10.10.png
代碼也就十分簡單了:

        //計算法線
        float3 calcNorm(float3 p)
        {
            float eps = 0.001;

            float3 norm = float3(
                map(p + float3(eps, 0, 0)) - map(p - float3(eps, 0, 0)),
                map(p + float3(0, eps, 0)) - map(p - float3(0, eps, 0)),
                map(p + float3(0, 0, eps)) - map(p - float3(0, 0, eps))
            );

            return normalize(norm);
        }

我們可以把法線信息輸出成顏色,就得到了下圖中的結(jié)果。
屏幕快照 2018-06-11 下午5.36.24.png

而實現(xiàn)一個簡單的漫反射也是一件十分簡單的事情:

          ret = dot(-_LightDir, calcNorm(p));
          ret.a = 1;

這樣我們就獲得一個有簡單光照效果的六棱體了。
屏幕快照 2018-06-11 下午5.44.59.png

0x03 陰影

六棱體上有了簡單的漫反射效果,接下來就要在此基礎(chǔ)上實現(xiàn)基于SDF的陰影效果了。SDF的一個優(yōu)勢就在于場景內(nèi)的距離信息全都是可知的,因此可以很方便地用來實現(xiàn)類似陰影這樣的效果,并且可以根據(jù)距離來更自然地實現(xiàn)陰影的衰減,從而生成一個更加真實的陰影。
不過在此之前,我會將場景修改的稍微復(fù)雜一點,當(dāng)然,這里我只是增加了3個物體的SDF的定義——Sphere、Plane和Cube,并且簡單的修改下map函數(shù),重新組織了一下整個場景。

        float sdSphere(float3 rp, float3 c, float r)
        {
            return distance(rp,c)-r;
        }

        float sdCube( float3 p, float3 b, float r )
        {
          return length(max(abs(p)-b,0.0))-r;
        }

        float sdPlane( float3 p )
        {
            return p.y + 1;
        }

        float map(float3 rp)
        {
            float ret;
            float sp = sdSphere(rp, float3(1.0,0.0,0.0), 1.0);
            float sp2 = sdSphere(rp, float3(1.0,2.0,0.0), 1.0);
            float cb = sdCube(rp+float3(2.1,-1.0,0.0), float3(2.0,2.0, 2.0), 0.0);
            float py = sdPlane(rp.y);
            ret = (sp < py) ? sp : py;
            ret = (ret < sp2) ? ret : sp2;
            ret = (ret < cb) ? ret : cb;
            return ret;
        }

這樣,整個場景就變成了這個樣子,由2個球體和1個正方體以及一個平面組成。
屏幕快照 2018-06-12 下午2.28.17.png

接下來我們來實現(xiàn)陰影,其實陰影的形成本身也很簡單。沿著光線的方向,如果光線被某個表面遮擋則會在后面的表面上生成陰影。
那么在代碼中,一個簡單的基于SDF的陰影實現(xiàn)就很簡單了:針對到達(dá)物體表面的采樣點,以該點為起點,沿著光線來的方向,發(fā)射另一根射向光源的射線。如果這根射線也擊中了某個物體的表面,則證明該采樣點處于陰影之中——其實還是raymarching。
下面我們來完成一個最簡單的陰影實現(xiàn),即陰影中是統(tǒng)一的黑色。

        float calcShadow(float3 rayOrigin, float3 rayDirection)
        {
            int maxDistance = 64;

            float rayDistance = 0.01;

            for(rayDistance ; rayDistance < maxDistance;)
            {
                float3 p = rayOrigin + rayDirection * rayDistance;
                float surfaceDistance = map(p);
                if(surfaceDistance < 0.001)
                {
                    return 0.0;
                }

                rayDistance += surfaceDistance;
            }
            return 1.0;
        }

當(dāng)然這里需要注意的是,第一次迭代時不要直接把采樣點傳入到map中,否則的話會直接return。
ok,這樣一個很硬的陰影就創(chuàng)建好了,沒有多余的pass,沒有多余的貼圖,使用SDF創(chuàng)建陰影就是這么簡單。
屏幕快照 2018-06-12 下午3.41.36.png
大家都知道,陰影通常是由所謂的本影和半影組成的,其中本影主要指的是物體表面上那些沒有被光源直接照射的區(qū)域,呈現(xiàn)全黑的狀態(tài),而所謂的半影則是那些半明半暗的過渡部分??梢钥吹轿覀儗崿F(xiàn)的這種陰影其實只包括本影,而沒有半影的效果。
所以在這個純黑的本影的基礎(chǔ)上,再增加一些不是純黑的半影效果,那么最后的陰影會更加真實。所以接下來我們就要考慮,黑色本影之外的表面上的那些點的顏色了。
這時我們把距離的因素考慮進(jìn)去:

      ret = min(ret, 10 * surfaceDistance /rayDistance );

屏幕快照 2018-06-12 下午4.15.06.png
可以看到,這樣一來在之前純黑的本影之外,不再是像最初的實現(xiàn)中將影子直接截斷,而是多了一圈模糊的半影來過渡。
不過,我相信眼尖的你一定發(fā)現(xiàn)了一些問題。那就是Cube的半影部分出現(xiàn)了條帶狀的artifact。
WX20180612-162614@2x.png
這主要是由于在計算陰影的RayMarching的過程中,采樣出現(xiàn)了問題。
在今年的GDC上,Sebastian Aaltonen分享了一個新的方案來解決這個問題:
屏幕快照 2018-06-12 下午5.23.03.png
屏幕快照 2018-06-12 下午5.32.51.png

根據(jù)上一次的采樣D-1和這一次的采樣D的數(shù)據(jù),來計算或者是估算一個這條射線上距離SDF表面最近的點E,并用E來計算半影。
在分享中Sebastian也給出了他修改后的半影計算公式:

Triangulation formula: res = min(res, 
(r2*sqrt(4*(r1*r1)-h*h))*rcp(2*hprev)/(t-h*h*rcp(2*hprev))) 

事實上Inigo也已經(jīng)根據(jù)Sebastian的分享,改進(jìn)了他的SDF陰影的效果。下面我們就根據(jù)Inigo和Sebastian的實現(xiàn),在Unity中解決掉這個半影部分的條帶狀的artifact吧。

        //Adapted from:iquilezles
        float calcSoftshadow( float3 ro, float3 rd, float mint, float tmax)
        {

            float res = 1.0;
            float t = mint;
            float ph = 1e10;
            
            for( int i=0; i<32; i++ )
            {
                float h = map( ro + rd*t );
                float y = h*h/(2.0*ph);
                float d = sqrt(h*h-y*y);
                res = min( res, 10.0*d/max(0.0,t-y) );
                ph = h;
                
                t += h;
                
                if( res<0.0001 || t>tmax ) 
                    break;
                
            }
            return clamp( res, 0.0, 1.0 );
        }

其中ph是上一次采樣時的圓形的半徑,h是當(dāng)前這次的采樣的圓形半徑。
修改后的陰影效果:
屏幕快照 2018-06-12 下午5.49.57.png

0x04 后記

這樣,我們就在Unity中實現(xiàn)了SDF渲染以及基于SDF的陰影渲染,并且解決了討厭的條帶狀的artifact。

本文的項目可以在這里獲?。?br> https://github.com/chenjd/Unity-Signed-Distance-Field-Shadow

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    午夜色午夜视频之日本| 国产亚洲欧美另类久久久| 亚洲欧洲一区二区中文字幕| 噜噜中文字幕一区二区| 91人妻人人做人碰人人九色| 欧美不卡午夜中文字幕| 亚洲欧美日韩综合在线成成| 91欧美亚洲视频在线| 午夜视频免费观看成人| 午夜精品在线视频一区| 国产精品欧美激情在线播放| 亚洲欧美中文字幕精品| 国产又爽又猛又粗又色对黄| 扒开腿狂躁女人爽出白浆av| 在线观看视频日韩成人| 欧美亚洲综合另类色妞| 在线免费不卡亚洲国产| 在线观看免费视频你懂的| 欧美大粗爽一区二区三区| 亚洲成人免费天堂诱惑| 欧美精品久久99九九| 亚洲熟女少妇精品一区二区三区| 一本色道久久综合狠狠躁| 国产欧美日本在线播放| 国产一区二区三区色噜噜| 东京热男人的天堂久久综合| 成人免费视频免费观看| 欧美精品在线播放一区二区| 国产又大又黄又粗的黄色| 国产内射一级一片内射高清| 日本亚洲欧美男人的天堂| 国产在线小视频你懂的| 成年人视频日本大香蕉久久| 美女被草的视频在线观看| 在线免费国产一区二区| 国产成人一区二区三区久久| 欧美一二三区高清不卡| 午夜精品国产精品久久久| 久久热九九这里只有精品| 亚洲av熟女国产一区二区三区站| 日韩美女偷拍视频久久|