终于周末了!

接上一篇,这儿依然是谈论在 Unity 中动态操作 Mesh 的事儿。在一篇中,我们已经创建出了一个圆。如图:

圆

所以有了基础,现在想改变它的话,就不是什么比较麻烦的事儿了。因为构造出来的三角形顺序已经确定,改变 Mesh 的形状的话,只需要单独修改顶点数组就可以了。

而动态改变 Mesh 什么的,实际上就是在上一篇的基础上,给它 “Duang” 点特效而已。既然如此,我也不介意给它多加点。首先,我觉得这个圆平平坦坦地,不大好看,所以想给他加个描边效果。虽然《白猫传奇》里边没这效果,不过咱只是做个类似的东西而已,可没说完全仿造,所以加点自己的东西,也不无为可。

目前行里流行的给模型加描边效果的方法,不外乎如下几种:
1. 全屏后期处理
2. 单独模型沿法线挤出
3. 单独模型沿顶点挤出
4. 单独模型沿法线与顶点照比例挤出
其中第一项一般用于需要整个游戏所有模型都进行描边的情况,省时省力。后面几项都是用于单独模型的操作,另外还有一些优化方法,比如将法线之类的转入视空间避免基础轮廓随距离导致变化问题、前后都进行挤出以显示所有轮廓等。这些方法咱就不用了,毕竟我们画的那个圆,就纯粹是个平面,完全没这需要,也就不浪费那点计算了。

另外对于我们动态创建的模型,还有一个问题是单独沿法线或者顶点挤出都会产生问题(因为我们的模型就只有一个片),如果单独沿法线挤出,只会出现一个与原模型大小一致的圆,调整挤出距离也只能让这个圆与原来的模型距离增大而已。而如果单独沿顶点挤出的话,效果是有了,可又会完全与原模型重合,结果就是,原来的模型被挤出来的 “复制体” 给挡了个精光。不信的话,可以自己试试,我也给两个图吧:

沿顶点挤出
沿法线挤出

因为这个 Shader 不是本文的主题,所以就直接代码了,第一个 Pass 与一般的 Shader 没区别,所以就上一下第二个 Pass,另外注意下最开始是关闭了 ZWrite,剔除后面就可以了:

		Pass
		{
			Tags { "RenderType"="Opaque" }
			ZWrite On
			Cull Back
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"		

			fixed4 _BorderColor;
			float _BorderOffset;
			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal:NORMAL;
			};

			float4 vert (appdata v):SV_POSITION
			{
				v.vertex.xyz+=v.vertex.xyz*_BorderOffset;
				v.vertex.z+=v.normal.z*0.1;
				return mul(UNITY_MATRIX_MVP, v.vertex);
			}
			
			fixed4 frag() : SV_Target
			{
				return _BorderColor;
			}
			ENDCG
		}

上图:

背景白色,挤出黑色


好了,接下来就是对顶点的操作了。

看《白猫传奇》那上边的圆圈,明显是就是手指点那儿,那儿就出现圆,然后根据手指的滑动,将面向手指的的圆上顶点,按照与手指的角度移动相应的距离。

这儿我们就略过隐藏显示模型的阶段了,那太简单了,完全都没啥必要说的了,最简单的方式就是直接一个 SetActive 解决 (虽然说实话真这样干似乎会有点问题)。不过我这儿不需要,所以略过此步。
方法已经知道,从理论上来说,就是上边我推测的那样了。判断角度我们可以用点积,距离可以直接从手指到顶点的向量进行比较。那么,下边就开始码代码吧。

首先,添加一个数组,用于缓存我们创建出来的初始顶点,bool 变量判断手指是否移动,以及一个 Vector3 保存手指,在这儿或者更应该说是鼠标的当前位置。最后,是一个 moveSpeed,控制了顶点向鼠标移动的速度:

s
private Vector3[] m_vertexBake;
    private bool m_isMove = false;
    private Vector3 m_currentPos;
    [SerializeField]
    private float m_moveSpeed = 15f;

接下来,我打算使用三个函数来解决这个问题:
OnMouseDown:当鼠标按下,记录当前位置 (哦,我们这儿不需要,所以我把它删了,注意如果有像《白猫传奇》那样的需要的话,就得记录一下初始位置,然后将圆创建到这儿),并将 isMove 置为 true
OnMouseMove:判断 isMove 是否为 true,若是,那么进行顶点位置的更新
OnMouseUp:当鼠标放开,将顶点归位

这三个函数都是在 Update () 中进行调用:

s
void Update()
    {
        OnMouseDown();
        OnMouseMove();
        OnMouseUp();
    }

首先是 OnMouseDown:

s
private void OnMouseDown()
    {
        if (Input.GetMouseButtonDown(0))
        {
            m_isMove = true;
            m_recover = false;
        }
    }

接下来是 OnMouseMove,若当前处于移动状态中,将鼠标位置转入世界坐标,更新鼠标当前位置,然后遍历顶点组,通过判断顶点与鼠标的角度计算顶点将会朝鼠标移动的距离:

s
private void OnMouseMove()
    {
        if (!m_isMove) return;
        m_currentPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        m_currentPos.z = 0;
        for (int i = 1; i < m_vertexBake.Length; i++)
        {
            float doValue = Mathf.Clamp01(Vector3.Dot((m_vertexBake[i] - m_vertexBake[0]).normalized, (m_currentPos - m_vertexBake[0]).normalized) - 0.1f);
            m_vertexList[i] = Vector3.Lerp(m_vertexList[i], m_vertexBake[i] + (m_currentPos - m_vertexBake[i]) * doValue, Time.deltaTime * m_moveSpeed);
        }
        m_mesh.SetVertices(m_vertexList);
    }

最后,OnMouseUp,当鼠标放开,将模型顶点归位:

s
private void OnMouseUp()
    {
        if (Input.GetMouseButtonUp(0))
        {
            m_isMove = false;
            m_mesh.vertices = m_vertexBake;
        }
    }

好了,效果如下:

效果

不过,明眼人可以发现:为什么方向与鼠标最一致的顶点,没有完全与鼠标位置重合呢?

解释就在这一句代码上:

s
float doValue = Mathf.Clamp01(Vector3.Dot((m_vertexBake[i] - m_vertexBake[0]).normalized, (m_currentPos - m_vertexBake[0]).normalized) - 0.1f);

答案就是我觉得那样太尖了,不好,所以稍微把它 “磨钝” 了一点。

最后我们要做的,就是为顶点恢复也做一个 lerp,否则大家都都看见了,顶点瞬间就回到原点的结果就是:难看。

为了这事儿,我们可以首先添加几个时间变量:

s
[SerializeField]
    // 顶点恢复的时间
    private float m_recoverTime = 0.5f;
    // 恢复的剩余时间
    private float m_recoverRemainTime = 0.5f;
    // 是否正处于恢复中
    private bool m_recover = false;

然后,改造一下 OnMOuseUp,每次鼠标放开,在这儿都会将是否恢复 m_recover 置为 true,并且重置恢复时间:

s
private void OnMouseUp()
    {
        if (Input.GetMouseButtonUp(0))
        {
            m_isMove = false;
            m_recover = true;
            m_recoverRemainTime = m_recoverTime;
        }
    }

然后,添加一个 RecoverVertex 方法,放在 Update 最后调用:

s
void Update()
    {
        OnMouseDown();
        OnMouseMove();
        OnMouseUp();
        RecoverVertex();
    }
	// 用于将顶点缓慢恢复
    private void RecoverVertex()
    {
    	// 若确定处于未恢复状态
        if (m_recover)
        {
            m_recoverRemainTime -= Time.deltaTime;
            // 恢复时间已完,退出
            if (m_recoverRemainTime < 0)
            {
                m_recover = false;
                m_mesh.vertices = m_vertexBake;
                return;
            }
            for (int i = 0; i < m_vertexBake.Length; i++)
            {
            	// 根据恢复时间的比率,将顶点恢复
                m_vertexList[i] = Vector3.Lerp(m_vertexList[i], m_vertexBake[i], 1 - m_recoverRemainTime / m_recoverTime);
            }
            m_mesh.SetVertices(m_vertexList);
        }
    }

我把顶点移动速度调整为 3,恢复时间为 0.5f, 最后效果如下:

效果

呐,就是这样,是不是有点像史莱姆呢?