Unity-雪地效果的实现
最终效果
https://www.zhihu.com/video/1217959676639330304
本文使用unity实现雪地效果,主要参考了github上两个已有的demo,将其结合并并增加了一些自己东西。
本文主要讲雪地效果,简单说就是雪地踩脚印的实现,并非雪面的渲染。然后因为是一段时间之前写的,细节部分有些遗忘,所以争取主要把原理讲清楚,不会涉及太多代码细节。可以下载github的demo参考更改。
如果有任何错误,欢迎指正,谢谢!~
参考demo
Demo1: https://github.com/thnewlands/unity-deformablesnow
Demo2: https://github.com/TheBeach54/SnowSimulation
雪地效果最基本原理
雪地效果的根本就是要踩出脚印。而实现这个的原理分两步:第一步,在雪地plane的下方放置一个正交相机,从下往上拍摄,如下图。目的是实时获取上方物体生成一张深度图;第二步,根据上一步生成深度图,将雪地plane的顶点向下方位移,使用tessellation技术。参考Demo1就是根据这一基本原理实现。理解上不难哈。
本文实现流程
本文采用的方法比上文说的多两个步骤。一个是参考了demo2的方法,具体则是在下方放置两个摄像机,一个相机在第一帧的时候记录下初始地面深度,因为初始地面可以是地形,本身就有一定高度。另外一个相机记录实时拍摄的物体深度。这样根据两个深度就能在任何地形上计算出最后在真正的踩雪地方,将其结果保存在r通道内。当然需要两个正交相机参数完全相同,具体计算方式后面会说。结果如下图和视频:
初始地形为平面
初始地形为山丘
初始地形为山丘最终效果
https://www.zhihu.com/video/1217959942839558144
另外一个步骤则是根据计算后的深度值图,进行两次轮廓线检测,采用的是最基本简单的sobel算子,然后用g和b通道分别存储其颜色。这样做的目的是,可以根据最后的深度图的不同通道,将雪地分为基本雪面(无颜色值)、凸起部分(g通道)、侧边(b通道)、凹陷底部(a通道)四个部分,如图。只需要在tessellation的过程中,将g通道的部分向上位移顶点,与a通道操作相反,则可以形成踩雪路径左右两边的凸起部分。
然后根据不同的通道使用不同的diffuse、normal等贴图渲染即可得到不同的雪面渲染效果。在这之前可以适当的将深度图进行模糊处理,使边界柔和。同时在雪面渲染地shader中使用lerp函数对贴图进行插值。结果如下图:
不同通道颜色的深度值对应的雪面贴图
实现细节
相机部分
这是下方放置的正交相机的脚本的截图,是获得物体深度的相机,另外一个获得初始地形深度的相机大致相同,现在介绍其主要部分。代码如下:
/// <summary>
/// 两张RenderTexture(m_rtDepth 1&2)都是物体的深度值
/// 交替传入cameraReceiveMat和snowRenderMat是为了保持深度图不刷新,并在cameraReceiveMat的shader中剔除了比雪面高的深度
/// </summary
public class ObjectDepthCam : MonoBehaviour
{
public GameObject snowPlane;// 雪面plane主要
public Material cameraReceiveMat;// 相机处理深度图shader
public Material snowRenderMat;// 雪面渲染shader
public Material snowEdgeDetectMat;// 第一层轮廓线检测
public Material snowOutEdgeDetectMat;// 第二层轮廓线检测
public Shader showDepthShader;// 替代物体shader 检测深度
public Material Quad1;// 上面视频中左边的quad显示
public int renderTextureSize = 512;
public float snowFarPlane = 10.0f;
public float SnowMaxHeight = 0.3f;
private RenderTexture m_rtDepth;
private RenderTexture m_rtDepth2;
private Camera cam;
bool firstFrame = true;
bool rtFlag = true;
void Start()
{
TextureInitial();
cam = GetComponent<Camera>();
cam.depthTextureMode = DepthTextureMode.Depth;
cam.farClipPlane = snowFarPlane;
//scale ortho camera with snow plane assuming it&#39;s 1:1
cam.orthographicSize *= snowPlane.transform.localScale.x;
cam.nearClipPlane = 0.0f;
cam.orthographic = true;
cam.aspect = 1.0f;
cam.clearFlags = CameraClearFlags.Color;
cam.backgroundColor = Color.black;
cameraReceiveMat.SetFloat(&#34;_SnowMaxHeight&#34;, SnowMaxHeight);
snowRenderMat.SetFloat(&#34;_SnowMaxHeight&#34;, SnowMaxHeight);
}
/// <summary>
/// 两张RenderTexture交替传送,保证物体的DepthTex能够不刷新
/// </summary>
void UpdateCamera()
{
// We apply the init height calculated in the Start() and pass the floor height to the receiveSnow shader
if (!firstFrame)
{
cam.SetReplacementShader(showDepthShader, &#34;TerrainEffect&#34;);
if (rtFlag)
{
cameraReceiveMat.SetTexture(&#34;_MainTex&#34;, m_rtDepth);
cameraReceiveMat.SetTexture(&#34;_DepthTex&#34;, m_rtDepth2);
snowRenderMat.SetTexture(&#34;_ImprintTex&#34;, m_rtDepth2);
Quad1.SetTexture(&#34;_MainTex&#34;, m_rtDepth2);
cam.targetTexture = m_rtDepth2;
}
else
{
cameraReceiveMat.SetTexture(&#34;_MainTex&#34;, m_rtDepth2);
cameraReceiveMat.SetTexture(&#34;_DepthTex&#34;, m_rtDepth);
snowRenderMat.SetTexture(&#34;_ImprintTex&#34;, m_rtDepth);
Quad1.SetTexture(&#34;_MainTex&#34;, m_rtDepth);
cam.targetTexture = m_rtDepth;
}
}
}
void Update()
{
// Each frame we update the camera
UpdateCamera();
}
/// <summary>
/// 在android平台无法在OnPostRenderer里使用RenderTexture交换
/// 只能在OnRenderImage中传到destination中
/// </summary>
/// <param name=&#34;source&#34;></param>
/// <param name=&#34;destination&#34;></param>
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (!firstFrame)
{
var temp = RenderTexture.GetTemporary(m_rtDepth.width, m_rtDepth.height, 0, m_rtDepth.format);
var temp2 = RenderTexture.GetTemporary(m_rtDepth.width, m_rtDepth.height, 0, m_rtDepth.format);
if (rtFlag)
{
Graphics.Blit(m_rtDepth, temp, cameraReceiveMat);
Graphics.Blit(temp, temp2, snowOutEdgeDetectMat);
Graphics.Blit(temp2, destination, snowEdgeDetectMat);
}
else
{
Graphics.Blit(m_rtDepth2, temp, cameraReceiveMat);
Graphics.Blit(temp, temp2, snowOutEdgeDetectMat);
Graphics.Blit(temp2, destination, snowEdgeDetectMat);
}
RenderTexture.ReleaseTemporary(temp);
RenderTexture.ReleaseTemporary(temp2);
rtFlag = !rtFlag;
}
firstFrame = false; // not the first frame anymore
}
void TextureInitial()
{
m_rtDepth = new RenderTexture(renderTextureSize, renderTextureSize, 0);
m_rtDepth.antiAliasing = 2;
m_rtDepth.format = RenderTextureFormat.ARGB64;
m_rtDepth.useMipMap = false;
m_rtDepth2 = new RenderTexture(renderTextureSize, renderTextureSize, 0);
m_rtDepth2.antiAliasing = 2;
m_rtDepth2.format = RenderTextureFormat.ARGB64;
m_rtDepth2.useMipMap = false;
}
}
首先,下方相机获取深度图,这一步通过SetReplacementShader替换上方物体的材质为SnowDepthShader获得,另外一个相机。SnowDepthShader中最基本的一个步骤就是讲fragment shader中的颜色值替换为o.rgb = float3(1 - i.depth, 0,0);这样就能使得相机获取深度图。
然后则是传给CamRecieveMat,该材质的shader就是用来处理接受到的ObjDepthTex(物体深度图)和FloorDepthTex(初始地形深度图,另外一个相机生成)。然后根据这两张图生成一张计算后的深度值图。计算代码如下:
float currentFloorBias = (floor - current.x) / (_SnowMaxHeight / _SnowFarPlane);
current.x = 1 - currentFloorBias;其中floor是初始地形的深度值,current.x是当前拍摄到的物体深度值,_SnowMaxHeight似是设定的雪面高度,_SnowFarPlane是相机的远平面距离,最终将计算出的深度值重新赋值给current.x。这样生成的计算后的深度值图,就能避免原始物体深度图中,在雪面以上的深度值被提取。
最后则是在OnRenderImage中进行两次后处理,分别用EdgeDetectMat和EdgeDetectMatOut,也就是两次sobel算子的轮廓线检测,分别存储到g和b通道内部。
另外一个重点则是我们使用了两张RT分辨存储深度值,然后在Update中交替传给各种mat。这样做主要是因为,一张RT存储当前帧的深度图,另外一张RT
存储上一帧的深度图,这样就能让深度图在CamRecieveMat中保持形成轨迹。
雪面渲染材质
其中主要的参数有:
Edge Length: Tessellation的程度Displacement Textur: 初始时让雪面适当位移的噪声贴图Imprint Texture: Camera脚本传递进来的最后的深度图Top Material Settings: 基本雪面(无颜色值部分)的雪面渲染Bottom Material Settings: 凹陷雪面(r通道部分)的雪面渲染Upper Material Settings: 凸起部分(g通道部分)的雪面渲染Middle Material Settings: 侧边雪面(b通道部分)的雪面渲染
然后还有一些关于个部分雪面渲染的程度等设定不赘述。
对于凸起部分的雪面贴图仅采用了不同的diffuse贴图和法线贴图,并附加不同的颜色值和法线程度。
Tessellation
根据上文所获得的最终深度图,直接使用unity提供的tessellation技术(不赘述),在disp函数里将顶点的y方向进行相对应的位移。不过需要注意的是需要重新计算法线方向,否则阴影会有问题。
另外还有一个trick,可以将只需要顶点位移的地方tessellation的值提高。但是这样做需要在剩余没有脚印的雪面部分变化不大,没有明显的位移,否则在不同tessellation的接缝处会产生裂痕。所以实际上最后我也没有用,只是一个参考。
三向贴图
没有使用三项贴图
使用了三向贴图
最后的一个处理是在雪面的渲染部分,地形变形导致了贴图的拉伸,因此采用三向贴图的算法进行贴图(具体原理连接https://blog.csdn.net/liu_if_else/article/details/73833656)。左图是普通uv贴图,靠近雪面边沿的地方贴图会有明显的拉伸。右图是三项贴图处理过后,贴图拉伸变形效果消除。只需要针对所有的贴图处理一下即可,代码如下:
fixed4 triPlanarTex(sampler2D tex, v2f i)
{
float3 blending = abs(i.normal);
blending = normalize(pow(blending, _TriplanarBlendSharpness));
float b = (blending.x + blending.y + blending.z);
blending /= float3(b, b, b);
float4 xaxis = tex2D(tex, i.w_Vertex.yz / _TextureScale);
float4 yaxis = tex2D(tex, i.w_Vertex.xz / _TextureScale);
float4 zaxis = tex2D(tex, i.w_Vertex.xy / _TextureScale);
fixed4 texcol = xaxis * blending.x + yaxis * blending.y + zaxis * blending.z;
return texcol;
}
最后的最后,本文是参考了两个demo融合更改的雪面效果,如果有错误,希望大家指正,谢谢~
页:
[1]