终于周末了!
接上一篇,这儿依然是谈论在 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,控制了顶点向鼠标移动的速度:
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 () 中进行调用:
void Update() | |
{ | |
OnMouseDown(); | |
OnMouseMove(); | |
OnMouseUp(); | |
} |
首先是 OnMouseDown:
private void OnMouseDown() | |
{ | |
if (Input.GetMouseButtonDown(0)) | |
{ | |
m_isMove = true; | |
m_recover = false; | |
} | |
} |
接下来是 OnMouseMove,若当前处于移动状态中,将鼠标位置转入世界坐标,更新鼠标当前位置,然后遍历顶点组,通过判断顶点与鼠标的角度计算顶点将会朝鼠标移动的距离:
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,当鼠标放开,将模型顶点归位:
private void OnMouseUp() | |
{ | |
if (Input.GetMouseButtonUp(0)) | |
{ | |
m_isMove = false; | |
m_mesh.vertices = m_vertexBake; | |
} | |
} |
好了,效果如下:
不过,明眼人可以发现:为什么方向与鼠标最一致的顶点,没有完全与鼠标位置重合呢?
解释就在这一句代码上:
float doValue = Mathf.Clamp01(Vector3.Dot((m_vertexBake[i] - m_vertexBake[0]).normalized, (m_currentPos - m_vertexBake[0]).normalized) - 0.1f); |
答案就是我觉得那样太尖了,不好,所以稍微把它 “磨钝” 了一点。
最后我们要做的,就是为顶点恢复也做一个 lerp,否则大家都都看见了,顶点瞬间就回到原点的结果就是:难看。
为了这事儿,我们可以首先添加几个时间变量:
[SerializeField] | |
// 顶点恢复的时间 | |
private float m_recoverTime = 0.5f; | |
// 恢复的剩余时间 | |
private float m_recoverRemainTime = 0.5f; | |
// 是否正处于恢复中 | |
private bool m_recover = false; |
然后,改造一下 OnMOuseUp,每次鼠标放开,在这儿都会将是否恢复 m_recover 置为 true,并且重置恢复时间:
private void OnMouseUp() | |
{ | |
if (Input.GetMouseButtonUp(0)) | |
{ | |
m_isMove = false; | |
m_recover = true; | |
m_recoverRemainTime = m_recoverTime; | |
} | |
} |
然后,添加一个 RecoverVertex 方法,放在 Update 最后调用:
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, 最后效果如下:
呐,就是这样,是不是有点像史莱姆呢?