中級Shader教程09 3D Raymarch框架

終於,我們暫時結束了2D,進入了令人興奮的3D!!
在2D的屏幕中,繪製3D場景—-升維進化!相信我,當你搞定了3D,再回頭看2Dshader,你會想起一句廣告詞,so easy!媽媽再也不用擔心我們的shader了.


1.3D Raymarch框架

  1. 獲得相機位置ro
  2. 根據相機位置和朝向,計算當前像素所發出的射線ray的方向rd(ray dir)
  3. 求交ray和場景的碰撞點p(兩種方式)
    3.1 直接算式求解(比如射線到一個簡單的圓的交點)
    3.2 使用raymarching方式即一步步的遞進ray,直到ray碰到場景,或達到ray的最大距離。
  4. 求得p處的法線和材質信息
  5. 根據4得到的信息求的p處的顏色

舉個Raymarching方式例子:

// create by JiepengTan 
// date:2018-04-12 
// email: [email protected]
Shader "FishManShaderTutorial/RayMarchSimpleScene"{
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader{
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            //#include "ShaderLibs/Noise.cginc"
            struct appdata{
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v){
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            #define SPHERE_ID (1.0)
            #define FLOOR_ID (2.0)
            #define lightDir (normalize(float3(5.,3.0,-1.0)))


            float MapSphere(float3 pos){
                // center at float3(0.,0.,0.);
                float radius = 0.5;
                float3 centerPos = float3(0.,1.0+ sin(_Time.y*1.)*0.5,0.);
                return length(pos-centerPos) - radius;
            }
            float MapFloor(float3 pos ){
                float3 n= float3(0.,1.,0.);
                float3 d = 0;
                return dot(n,pos)-d;
            }
            float2 Map(float3 pos){
                float dist2Sphere = MapSphere(pos);// ID 1
                float dist2Plane = MapFloor(pos); // ID 2
                if(dist2Plane < dist2Sphere) {
                    return float2(dist2Plane,FLOOR_ID);
                }else{
                    return float2(dist2Sphere,SPHERE_ID);
                }
            }


            #define MARCH_NUM 256 //最多光線檢測次數
            float2 RayCast(float3 ro,float3 rd){
                float tmin = 0.1;
                float tmax = 20.0;

                float t = tmin;
                float2 res = float2(0.,-1.0);
                for( int i=0; i<MARCH_NUM; i++ )
                {
                    float precis = 0.0005;
                    float3 pos = ro+rd*t;
                    res = Map(pos);
                    if( res.x<precis || t > tmax ) break;
                    t += 0.5*res.x;// 加速檢測速度 這裏可以有不同的策略
                }
                if( t>tmax ) return float2(t,-1.0);
                return float2( t, res.y );
            }
            float SoftShadow(float3 ro, float3 rd )
            {
                float res = 1.0;
                float t = 0.001;
                for( int i=0; i<80; i++ )
                {
                    float3  p = ro + t*rd;
                    float h = Map(p);
                    res = min( res, 16.0*h/t );
                    t += h;
                    if( res<0.001 ||p.y>(200.0) ) break;
                }
                return clamp( res, 0.0, 1.0 );
            }

            float3 ShadingShpere(float3 rd,float3 pos, float3 n,float3 sd){
                float3 col = float3(1.,0.,0.);
                float diff = clamp(dot(n,lightDir),0.,1.);
                float bklig = clamp(dot(n,-lightDir),0.,1.)*0.05;//加點背光
                return col *(diff+bklig);
            }
            float3 ShadingFloor(float3 rd,float3 pos, float3 n,float3 sd ){
                float3 col = float3(0.,1.,0.);
                float diff = clamp(dot(n,lightDir),0.,1.);
                return col *diff*sd;
            }
            float3 ShadingBG(float3 rd,float3 pos, float3 n ){
                float val = pow(rd.y,2.0);
                float3 bCol =float3(0.,0.,0.);
                float3 uCol =float3(0.1,0.2,0.9);
                return lerp(bCol,uCol,val);
            }
            float3 Shading(float3 rd,float3 pos, float3 n ,float matID){
                float sd = SoftShadow(pos,lightDir);
                if(matID >= (FLOOR_ID-0.5)){
                    return ShadingFloor(rd,pos,n,sd);
                }else{
                    return ShadingShpere(rd,pos,n,sd);
                }
            }

            float3 Normal(float3 pos, float t){
                float val = 0.0001 * t*t;
                float3 eps = float3(val,0.,0.);
                float3 nor = float3(
                    Map(pos+eps.xyy).x - Map(pos-eps.xyy).x,
                    Map(pos+eps.yxy).x - Map(pos-eps.yxy).x,
                    Map(pos+eps.yyx).x - Map(pos-eps.yyx).x );
                return normalize(nor);
            }
            void SetCamera(float2 uv,out float3 ro, out float3 rd){
                //步驟1 獲得相機位置ro
                ro = float3(0.,2.,-5.0);//獲取相機的位置 
                float3 ta = float3(0.,0.5,0.);//獲取目標位置
                float3 forward = normalize( ta - ro);//計算 forward 方向
                float3 left = normalize(cross( float3(0.0,1.0,0.0), forward ));//計算 left 方向
                float3 up = normalize(cross(forward,left));////計算 up 方向
                const float zoom = 1.;

                //步驟2 獲得射線朝向
                rd = normalize( uv.x*left + uv.y*up + zoom*forward );
            }
            fixed4 frag (v2f i) : SV_Target
            {
                // map uv into [-0.5,0.5]
                float2 uv = (i.uv-0.5) * float2(_ScreenParams.x/_ScreenParams.y,1.0);
                float3 ro,rd;
                //步驟1 步驟2
                SetCamera(uv,ro,rd);
                //步驟3 求交ray和場景的碰撞點p 
                float2 ret = RayCast(ro,rd);
                float3 pos = ro+ret.x*rd;

                //步驟4 計算碰撞點的法線信息    
                float3 nor= Normal( pos, ret.x );

                //步驟5 使用步驟4獲得的信息計算當前像素的的顏色值
                float3 col = Shading(rd, pos,nor,ret.y);
                if(ret.y < -0.5){
                    col = ShadingBG(rd,pos,nor);
                }
                return float4(col,1.0);
            }


            ENDCG 
        }//end pass
    }//end SubShader
    FallBack Off
}

2.公用框架抽取

2.1.抽取SetCamera函數

和shadertoy不同的是,unity 爲我們提供好了一個非常棒的場景編輯器,爲了更加直觀的操作和觀察我們的shader場景,我們可以直接將unity中相機的信息穿進去。來初始化ro,rd。

2.2.融合Unity場景和RayMarching場景

光柵化渲染的優點是非常高效的渲染任意形態的多邊形,而raymarching的或者說是SDF的優點是非常擅長處理規則,公式化的場景。爲了利用這兩種不同的渲染方式的優點。我們希望能夠將Unity渲染的場景和raymarching渲染的場景整合在一起。
這個整合的一個切入點是,目前光柵化硬件的處理方式是通過zbuffer的方式來實現多層物體的正確前後排序。整個場景通過投影到近裁剪面的方式來渲染。最終zBuffer中保存的就是在目標像素髮出的射線所碰撞的離相機最近的不透明的點到相機的向量投影到相機forward軸的長度。
從另外一種角度來看,光柵是一宗raymarching的逆形式。(一種是ray朝向相機,一種是ray遠離相機)
從相機的參數中我們可以獲取rd相關信息,從zbuffer獲取的值中我們可以計算得到射線到碰撞點的距離uz(unity z)。結合raymarching 中計算得到的碰撞點到相機的距離rz(ray z),類似光柵中的ztest,我們比較uz和rz 選取較小的z值對於的shading值。

所以本教程使用的一種實現方式:
1.獲取rd
通過相機的參數,計算相機近裁減面的四個角到相機的射線,(渲染的是一個四邊形)然後通過頂點shader中採樣,利用光柵化的過程,硬件加速插值來得到射線rd。

2.計算rz
從unity中獲取深度貼圖,並採樣得到zVal,然後通過投影逆操作,計算得到rz

float depth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
float rz = depth * length(i.interpolatedRay.xyz);//

3.獲取sceneColor
從unity中獲取ColorBuffer信息(使用 OnRenderImage+RenderTexture),

float4 ProcessRayMarch(float2 uv,float3 ro,float3 rd,inout float sceneDep,float4 sceneCol);
float4 frag(v2f i) : SV_Target{
    float depth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
    float rz = depth * length(i.interpolatedRay.xyz);//
    fixed4 sceneCol = tex2D(_MainTex, i.uv);//獲取Unity渲染結果
    float2 uv = i.uv * float2(_ScreenParams.x/_ScreenParams.y,1.0);
    fixed3 ro = _WorldSpaceCameraPos;
    fixed3 rd = normalize(i.interpolatedRay.xyz);//注意需要normalize
    return ProcessRayMarch(uv,ro,rd,rz,sceneCol);
}

將其封裝後在Framework3D.cginc中,配合宏展開,我們將不再需要重寫這段代碼

#pragma vertex vert   
#pragma fragment frag  
#include "ShaderLibs/Framework3D.cginc"
float4 ProcessRayMarch(float2 uv,float3 ro,float3 rd,inout float sceneDep,float4 sceneCol)  {
 ...//你自己的渲染代碼
}

4.比較zVal,選擇最終需要渲染結果
有兩種可能,一種是我們是將Unity渲染的場景作爲主場景,RayMarching只是當成是一種PostProcess shader,這時候,我們不將RayMarching 的背景色寫入ColorBuffer,使用MergeRayMarchingIntoUnity,例如Fog場景
第二種是raymarching 是主場景,unity只是作爲點綴。這時候,unity中的天空盒信息就不應該寫入ColorBuffer,應該使用MergeUnityIntoRayMarching。例如Lake in Highland

//RayMarching is the main world, ignore unity sky box
void MergeUnityIntoRayMarching(inout float rz,inout float3 rCol, float unityDep,float4 unityCol){
    if(rz>unityDep && unityDep<_ProjectionParams.z-1.){// unity camera far plane 
        rCol = unityCol.xyz;
        rz = unityDep;
    }
}

//Unity scene is the main world, ignore RayMarching sky box
void MergeRayMarchingIntoUnity(inout float rz,inout float3 rCol, float unityDep,float4 unityCol){
    if(rz>unityDep ){
        rCol = unityCol.xyz;
        rz = unityDep;
    }
}       

2.3.raymarching公共代碼抽取

1.如在上面框架中描述了基本的raymarching 流程,其中有幾個函數是每個raymarching 都共用的,所以這裏提取出來幾個宏,減少重複代碼

//計算法線
#define _MACRO_CALC_NORMAL(pos,rz, MAP_FUNC)\
    float2 e = float2(1.0,-1.0)*0.5773*0.002*rz;\
    return normalize( e.xyy*MAP_FUNC( pos + e.xyy ).x + \
                        e.yyx*MAP_FUNC( pos + e.yyx ).x + \
                        e.yxy*MAP_FUNC( pos + e.yxy ).x + \
                        e.xxx*MAP_FUNC( pos + e.xxx ).x );

//計算shadow
#define _MACRO_SOFT_SHADOW(ro, rd, maxH,MAP_FUNC) \
    float res = 1.0;\
    float t = 0.001;\
    for( int i=0; i<80; i++ ){\
        float3  p = ro + t*rd;\
        float h = MAP_FUNC( p).x;\
        res = min( res, 16.0*h/t );\
        t += h;\
        if( res<0.001 ||p.y> maxH ) break;\
    }\
    return clamp( res, 0.0, 1.0 );

//raycast
#define _MRCRO_RAY_CAST( ro, rd ,tmax,MAP_FUNC)\
    float t = .1;\
    float m = -1.0;\
    for( int i=0; i<256; i++ ) {\
        float precis = 0.0005*t;\
        float2 res = MAP_FUNC( ro+rd*t );\
        if( res.x<precis || t>tmax ) break;\
        t += 0.8*res.x;\
        m = res.y;\
    } \
    if( t>tmax ) m=-1.0;\
    return float2( t, m );

3.另外一種3D渲染實現

在這個框架的基礎上,我們實現上面所提到的使用公式的方式計算ray到場景的碰撞點的方式。
省去了RayCast部分,整個渲染代碼非常的短.(當然也因爲場景簡單,以及shading過程簡單)

Shader "FishManShaderTutorial/RaymarchMergeExample" {
    Properties{
        _MainTex("Base (RGB)", 2D) = "white" {}
    }
    SubShader{
        Pass {
            ZTest Always Cull Off ZWrite Off
            CGPROGRAM

#pragma vertex vert   
#pragma fragment frag  
#include "ShaderLibs/Framework3D.cginc"

            float4 ProcessRayMarch(float2 uv,float3 ro,float3 rd,inout float sceneDep,float4 sceneCol)  {
                float3 col = float3(0.,0.,0.);
                float3 n = float3(0.,1.0,0.);
                float t = sceneDep + 10;
                float occ = 0.;
                float3 sc =  float3(0.,1.0+0.5*sin(ftime*PI2),0.);
                float3 sr = 0.5;
                float3 ce = sc - ro;
                float b = dot( rd, ce );
                float tt = sr*sr - (dot( ce, ce )- b*b );
                if( tt > 0.0 ){
                    t = b - sqrt(tt);
                    float3 p = ro+t*rd;
                    col = 0.5+0.5*cos(2.*PI*(float3(1.,1.,1.)*p.y*0.2+float3(0.,0.33,0.67)));
                }
                MergeRayMarchingIntoUnity(t,col, sceneDep,sceneCol);
                return float4(col,1.0);
            } 
            ENDCG
        }//end pass
    }//end SubShader
    FallBack Off
}

效果:

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