Shader学习笔记 04 - 积雪
积雪判定
- 同一坐标系下法向量与雪的方向点乘,可以控制雪的方向
- 使用世界坐标系法线的g通道值,无法控制雪方向,省了点乘操作
雪颜色
- 纹理贴图:通过世界坐标的xz值采集,需要无缝贴图(seamless texture)
- 半兰伯特(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;
}