作者个人主页: https://zhuanlan.zhihu.com/p/34806560,同时,作者也是U Sparkle活动参与者哦,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!
这篇文章主要和大家聊一聊带波浪的地面的实现,以及场景特效的实现方法。如下图所示,地板随时间以波浪状变化,角色在地板上走的时候会随着它的波浪高低起伏。
视觉实现
关于波浪效果的实现,作者最开始的时候走了些弯路。一开始,我想的是试试用直接修改地板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/pac ... 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
- }
复制代码
最后的成果展示:
文末,再次感谢邓佳迪的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨(QQ群:465082844)。也欢迎大家来积极参与U Sparkle开发者计划,简称"US",代表你和我,代表UWA和开发者在一起!
关于UWA:
由侑虎科技开发的游戏/VR应用性能优化平台,目前提供 1)性能诊断与优化 2)资源检测与分析 3)UWA GOT 三大工具,帮助开发者在短时间内大幅度提升性能表现;同时其搭建的知识分享的博客和答疑解惑的互动平台使广大开发者收益。
|