找回密码
 立即注册
查看: 437|回复: 0

Unity-雪地效果的实现

[复制链接]
发表于 2020-11-24 10:07 | 显示全部楼层 |阅读模式

最终效果
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显示

    [Tooltip("动态生成的贴图大小")]
    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'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("_SnowMaxHeight", SnowMaxHeight);
        snowRenderMat.SetFloat("_SnowMaxHeight", 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, "TerrainEffect");
            if (rtFlag)
            {
                cameraReceiveMat.SetTexture("_MainTex", m_rtDepth);
                cameraReceiveMat.SetTexture("_DepthTex", m_rtDepth2);
                snowRenderMat.SetTexture("_ImprintTex", m_rtDepth2);
                Quad1.SetTexture("_MainTex", m_rtDepth2);
                cam.targetTexture = m_rtDepth2;
            }
            else
            {
                cameraReceiveMat.SetTexture("_MainTex", m_rtDepth2);
                cameraReceiveMat.SetTexture("_DepthTex", m_rtDepth);
                snowRenderMat.SetTexture("_ImprintTex", m_rtDepth);
                Quad1.SetTexture("_MainTex", m_rtDepth);
                cam.targetTexture = m_rtDepth;
            }
        }
    }

    void Update()
    {
        // Each frame we update the camera
        UpdateCamera();
    }

    /// <summary>
    /// 在android平台无法在OnPostRenderer里使用RenderTexture交换
    /// 只能在OnRenderImage中传到destination中
    /// </summary>
    /// <param name="source"></param>
    /// <param name="destination"></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融合更改的雪面效果,如果有错误,希望大家指正,谢谢~

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-19 11:52 , Processed in 0.098365 second(s), 24 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表