1. 程式人生 > 實用技巧 >Shader學習筆記 04 - 積雪

Shader學習筆記 04 - 積雪

積雪判定
  1. 同一座標系下法向量與雪的方向點乘,可以控制雪的方向
  2. 使用世界座標系法線的g通道值,無法控制雪方向,省了點乘操作
雪顏色
  1. 紋理貼圖:通過世界座標的xz值採集,需要無縫貼圖(seamless texture)
  2. 半蘭伯特(half Lambert)+ 漸變紋理
雪厚度

將頂點依法線向外偏移(非後處理雪效果)。

float _Height;
vert {
    if(積雪判定){
        vertex.xyz += normal * _Height;
    }
}
參考

https://www.patreon.com/posts/15944770,dot判斷,半蘭伯特+漸變紋理(卡通著色)

https://blog.theknightsofunity.com/make-it-snow-fast-screen-space-snow-shader,後處理雪效果


以上的積雪判定是無法判斷室內外,遮擋物下的物體也會有積雪效果。

判斷遮擋的積雪判定需要一個在天空上面向地面的正交相機渲染深度圖,渲染時反推出當前正交相機座標系下的深度值,然後與深度圖中所在的深度值作比較。

1. 正交相機渲染深度圖

深度圖的精度要求不高,且同一地點一般只需要更新一次,可以根據主相機的移動來分段更新。

// C#
RenderTexture depthTexture;
Camera skyDepthCam;
Shader replacementShader = null;
start {
    // skyDepthCam所在的game object最好使用掛載在主相機的指令碼中生成,這樣不需要跨物件獲取引數,解耦
    // skyDepthCamObj = new GameObject("skyDepthCamObj");
    // skyDepthCam = skyDepthCamObj.AddComponent<Camera>();
    var skyDepthCam = GetComponent<Camera>();
    gameObject.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
    skyDepthCam.enabled = false;
    skyDepthCam.renderingPath = RenderingPath.Forward;
    skyDepthCam.orthographic = true;
    skyDepthCam.clearFlags = CameraClearFlags.SolidColor;
    skyDepthCam.allowMSAA = false;
    skyDepthCam.backgroundColor = new Color(1f, 0f, 0f, 0f);
    skyDepthCam.nearClipPlane = 1f;
    skyDepthCam.orthographicSize = 256f; // 相機渲染大小
    skyDepthCam.transform.rotation = Quaternion.Euler(90, 0, 0); // 面垂直朝下
    if (depthTexture == null)
    {
        var res = 512;  // 深度圖精度,最好是orthographicSize的一倍
        depthTexture = new RenderTexture(res, res, 24, RenderTextureFormat.RGFloat, RenderTextureReadWrite.Linear);
        depthTexture.hideFlags = HideFlags.DontSave;
        depthTexture.filterMode = FilterMode.Bilinear;
        depthTexture.wrapMode = TextureWrapMode.Clamp;
        depthTexture.Create();
    }
    skyDepthCam.targetTexture = depthTexture;
    // 使用替換shader渲染深度圖
    replacementShader = Shader.Find("RenderDepthSh");
    if (replacementShader == null)
    {
        Debug.LogError("could not find 'RenderDepth' shader");
    }
    skyDepthCam.RenderWithShader(replacementShader, null);
}

// Shader,RenderDepthSh,替換shader渲染深度圖
vert {
    o.depth = COMPUTE_DEPTH_01;
}
frag {
	return i.depth;
}
2. 主相機渲染

主相機渲染需要世界座標才能獲取到正交相機所對應的深度值。

正交相機預設高寬比是1:1(camera.aspect),著色器中通過世界座標xz值減去左下點的值在除以寬度(2*orthographicSize)即可獲取uv正交相機的深度圖的UV值,以此獲取正交相機座標系下積雪點的深度值。世界座標系正交相機的y減去當前頂點的y值即為當前頂點在正交相機座標系下的深度值,如果大於積雪點的深度值,則當前頂點在遮擋物下面。

判斷結果,白色積雪點,黑色未障礙物下

通過深度反推的世界座標無法使用(效果不好且不穩定,猜測精度不夠),所以使用向前渲染路徑中的世界座標。在每個shader中加上積雪效果不現實而且也不相容延遲渲染路徑。解決方法是拷貝當前相機,使用替換shader渲染積雪效果得出渲染貼圖。然後在後處理中與渲染結果合併。

// c#
// 設定用於渲染雪的相機
var currentCam = GetComponent<Camera>();
var snowCamObj = new GameObject("snowCamObj");
snowCamObj.hideFlags = HideFlags.HideAndDontSave;
var snowCam = snowCam.AddComponent<Camera>();
snowCam.enabled = false;
snowCam.CopyFrom(currentCam);
snowCam.renderingPath = RenderingPath.Forward;
snowCam.depthTextureMode = DepthTextureMode.None;
var snowedSceneTexture = RenderTexture.GetTemporary(snowCam.pixelWidth, snowCam.pixelHeight, 24, RenderTextureFormat.ARGB32);
snowCam.backgroundColor = new Color(0, 0, 0, 0);
snowCam.clearFlags = CameraClearFlags.SolidColor;
snowCam.allowMSAA = false;
snowCam.allowHDR = false;
snowCam.targetTexture = snowedSceneTexture;
snowCam.rect = new Rect(0, 0, 1f, 1f);
// 設定渲染用引數
Shader.SetGlobalTexture("_SkyDepthTexture", depthTexture); // 正交相機渲染出的深度貼圖
Shader.SetGlobalVector("_SkyCamPos", new Vector4(skyDepthCam.transform.position.x, skyDepthCam.transform.position.y, skyDepthCam.transform.position.z, skyDepthCam.orthographicSize)); 
var replacementShader = Shader.Find("SnowEffectSh");
if (replacementShader == null)
{
    Debug.LogError("could not find 'SnowEffectSh' shader");
}
snowCam.RenderWithShader(replacementShader, "RenderType");
// Shader
vert {
    worldPos = mul(unity_ObjectToWorld, vertex).xyz;
}
frag {
    float4 depUV = float4(worldPos.xz - (_SkyCamPos.xz-_SkyCamPos.w), 0, 0);
    depUV /= (_SkyCamPos.w*2.0);
    float dep1 = tex2Dlod(_SkyDepthTexture, depUV).r;   // 遮擋物的深度值
    float dep2 = max(_SkyCamPos.y - worldPos.y, 0.001); // 當前位置在正交相機座標系下的深度值
    float minDep = min(dep1, dep2);
    o.Alpha = saturate( ((minDep / dep2) - 0.9875) * 110.0); // 誤差修正,0:非積雪出,1:積雪處
    if(o.Alpha <= 0) {
        return;
    }
	
    。。。 // 獲取積雪顏色 SnowColor
	
    o.Albedo = SnowColor;
}