《三弦》技术分享—波纹效果及其实现

《三弦》技术分享—波纹效果及其实现

这篇文章主要和大家聊一聊带波浪的地面的实现,以及场景特效的实现方法。如下图所示,地板随时间以波浪状变化,角色在地板上走的时候会随着它的波浪高低起伏。
请输入图片描述

视觉实现

关于波浪效果的实现,作者最开始的时候走了些弯路。一开始,我想的是试试用直接修改地板Mesh的方式,达到视觉和物理上的同步实现。事实上这被证明是非常不科学的,因为修改Mesh会严重影响性能。在涉及物理的情况下,我测试的结果是游戏帧率调到了个位数。所以最终选择了利用Shader实现视觉效果+利用脚本伪造行走物理效果的办法。

首先是地板的模型,这里使用了一个Plane:
请输入图片描述
波浪函数的实现,这里是函数实现,用的是两个方向的波进行叠加,波纹随时间变化。即构造一个全局函数,因为c#代码和Shader代码里都要使用,所以这是一个以World-Space的坐标x,z,以及游戏时间Time.time为变量的,以波浪高度y为结果的函数。使用的波纹函数为三角函数:
请输入图片描述
两路波形线性叠加,具体实现如下:

    [SerializeField] float height1 = 1f;
    [SerializeField] float frequency1 = 1f;
    [SerializeField] float wavelength1 = 1f;
    [SerializeField] Vector3 wave1Direction;
    [SerializeField] float height2 = 1f;
    [SerializeField] float frequency2 = 1f;
    [SerializeField] float wavelength2 = 1f;
    [SerializeField] Vector3 wave2Direction;
    [SerializeField][Range(0,10f)] float overallHeight = 1f;
    [SerializeField][Range(0,10f)] float overallFrequency = 1f;
    [SerializeField][Range(0,10f)] float overallWaveLength = 1f;
    [SerializeField] float offset;  

        public float GetHeight( float x , float z )
    {
        float x1 = x * wave1Direction.normalized.x + z * wave1Direction.normalized.z;
        float y1 = Mathf.Sin (x1 / (wavelength1 * overallWaveLength) + Time.time * frequency1 * overallFrequency) * height1 * overallHeight;

        float x2 = x * wave2Direction.normalized.x + z * wave2Direction.normalized.z;
        float y2 = Mathf.Sin (x2 / (wavelength2 * overallWaveLength) + Time.time * frequency2 * overallFrequency) * height2 * overallHeight;

        return y1 + y2 + offset;
    }

当然这是在C#里的实现,其实和在Shader里的实现类似。不过注意,需要把函数的参数传给Material。同时,需要每帧对时间参数进行同步(C#代码):

    public void SetMaterial( Material material ) // called at start
    {
        material.SetFloat ("_height1", height1 * overallHeight);
        material.SetFloat ("_frequency1", frequency1 * overallFrequency);
        material.SetFloat ("_waveLength1", wavelength1* overallWaveLength);
        material.SetFloat ("_height2", height2 * overallHeight);
        material.SetFloat ("_frequency2", frequency2 * overallFrequency);
        material.SetFloat ("_waveLength2", wavelength2* overallWaveLength);

        material.SetVector ("_wave1Direction", wave1Direction.normalized);
        material.SetVector ("_wave2Direction", wave2Direction.normalized);
        material.SetFloat ("_waveOffset", offset);
    }

        public void Update Material(Material material ) // called at update
        {
                material.SetFloat ("_Timer", Time.time);
        }

在Shader里,需要在Vert函数里,对顶点的位置进行重新计算:

v2f vert(appdata v )
{
    v2f o;
    o.uv = v.uv;

    o.worldPos = mul(unity_ObjectToWorld , v.vertex);
    o.worldPos.y = GetWaveHeight( o.worldPos.x , o.worldPos.z , _Timer);
    v.vertex.y = mul(unity_WorldToObject , o.worldPos).y;
    o.vertex = UnityObjectToClipPos( v.vertex  );
}

实现的效果如下:
请输入图片描述
至此,视觉部分已经完成。

这里把错误示范列出来,利用Mesh实现波浪效果的脚本:

public Mesh m_mesh;

void Update ()
{
    {
        List<Vector3> verticles = new List<Vector3> (m_mesh.vertices);

        for (int i = 0; i < verticles.Count; ++i) {
            var vect = verticles [i];
            vect.y = GetWaveHeight( vecticles[i].x , verticles[i].z );
            verticles [i] = vect;
        }

        m_mesh.SetVertices (verticles);
        m_mesh.RecalculateBounds ();
    }
}

逻辑实现

逻辑部分的实现也是绕了不少弯路。

一开始想的是直接更改Mesh来实现,但是消耗太大,故弃之。

接下来的思路还是利用物理系统,用一个Cube模拟玩家脚下的地面,每帧更新它的位置和角度信息。理论上来说,这种实现方法是可以模拟玩家和地表的物理碰撞。但只是理论,实际上这种实现方法会出现比较严重的抖动情况。(角色的控制使用的是自带的Character Controller)

最后的实现方法是,在角色控制的脚本里,直接对角色的位置进行修改。由于波纹函数是平滑的,所以实现的效果也可以做到比较平滑。同时,根据场地形的斜度对速度进行一定的调整,达到上坡下坡的效果。

float climbRate = Mathf.Clamp( WaveController.Instance.GetGradient ( Position.x , Position.z , m_MoveDir) * 10f + 1f  , 0.6f , 1.5f ) ;
speed *= climbRate;

斜率的计算直接用数值法求导(用导数法太麻烦了):

    public float GetGradient( float x , float z , Vector3 velocity )
    {
                // if the velocity is too small
        if (velocity.magnitude < Mathf.Epsilon) 
            return 0;
                
        float thisY = GetHeight (x, z);
        Vector3 delta = velocity * Time.deltaTime;
        float otherY = GetHeight (x + delta.x, z + delta.z);

                // the gradient is delta Y / delta V
        return - (otherY - thisY) / delta.magnitude;
    }

弄好之后,就可以愉快地用角色爬坡了~


后期特效实现

后期的特效实现的是一种反向Bloom的效果,下图是该效果夸张处理后的实现:
请输入图片描述
物体的黑色部分被加强,相当于一个反向的Bloom。

选择Standard Asset里的Optimized Bloom作为基础,进行修改。源代码在Asset Store上可以免费下载:
https://assetstore.unity.com/packages/essentials/legacy-image-effects-83913

首先分析Bloom的代码,查看BloomOptimized和在MobileBloom.Shader里,可以看到Bloom的流程是:

Downsample(提取颜色高亮的部分)->Blur(进行横竖方向的模糊)->Bloom(把高光颜色和原画面进行叠加)

根据这个流程,如果我们需要把Bloom效果反向,在Bloom部分把颜色进行剔除,同时需要在DownSample部分进行修改,进行曲线上的调整。

具体的做法如下:

fixed4 fragDownsample ( v2f_tap i ) : SV_Target
{               
    fixed4 color = tex2D (_MainTex, i.uv20);
    color += tex2D (_MainTex, i.uv21);
    color += tex2D (_MainTex, i.uv22);
    color += tex2D (_MainTex, i.uv23);
        //return max(color/4 - THRESHHOLD, 0) * ONE_MINUS_THRESHHOLD_TIMES_INTENSITY;
    // this curve is used for no reason, I just feel it looks good
        return ( color / 4 ) * THRESHHOLD + 1 - THRESHHOLD;
}

fixed4 fragBloom ( v2f_simple i ) : SV_Target
{   
        #if UNITY_UV_STARTS_AT_TOP      
    fixed4 color = tex2D(_MainTex, i.uv2);
        // return color + tex2D(_Bloom, i.uv);
        // combine the color by mutiply
    return color * max( 1 - ( 1 - tex2D(_Bloom, i.uv)) * INTENSITY , 0 );       
    #else
    fixed4 color = tex2D(_MainTex, i.uv);
        // return color + tex2D(_Bloom, i.uv);
    return color * max( 1 - ( 1 - tex2D(_Bloom, i.uv)) * INTENSITY , 0 );               
    #endif
} 

最后的成果展示:
请输入图片描述


这是侑虎科技第399篇文章,感谢作者邓佳迪供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。QQ群:793972859

作者个人主页:https://zhuanlan.zhihu.com/p/34806560,同时,作者也是U Sparkle活动参与者哦,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!