在游戏引擎场景中的3D物体是由一定数量的点、面组成的,如下图:
要使这些物体变形就是改变3D物体每个顶点状态。
1.首先在Unity场景中增加一个球体,如下图
3D组件默认拥有MeshFilter、meshRenderer、Collider组件,分别用来获取Mesh顶点、渲染物体、返回射线碰撞位置信息
新建物体形变脚本MeshDeformer,并在游戏开始时缓存形变的网格和顶点信息,新建完成后,将脚本挂载到要形变的物体上。
public class MeshDeformer : MonoBehaviour
{
//需要变形的mesh网格
Mesh deformingMesh;
//顶点原始位置,移动后的顶点位置
Vector3[] originalVertices, displacedVertices;
void Start()
{
//获取变形网格
deformingMesh = GetComponent<MeshFilter>().mesh;
//获取变形网格的所有顶点位置
originalVertices = deformingMesh.vertices;
displacedVertices = new Vector3[originalVertices.Length];
for (int i = 0; i < originalVertices.Length; i++)
{
displacedVertices[i] = originalVertices[i];
}
}
}
新建输入脚本MeshFormerInput,并在Update函数中检测输入
public class MeshDeformerInput : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButton(0))
{
HandleInput();
}
}
void HandleInput()
{
//获得从相机位置往鼠标点击屏幕点方向的射线
Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(inputRay, out hit))
{
MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();
if (deformer)
{
Debug.Log(deformer);
}
}
}
}
如果一切顺利则,在场景中点击物体就会在Unity控制台中打印获取到的组件信息
在MeshDeformer脚本中增加施加力的作用效果的方法,
/// <summary>
/// 给形变物体施加力
/// </summary>
/// <param name="point"></param>
/// <param name="force"></param>
public void AddDeformingForce(Vector3 point, float force)
{
for (int i = 0; i < displacedVertices.Length; i++)
{
//施加力到顶点
AddForceToVertex(i, point, force);
}
}
/// <summary>
/// 给某个顶点添加力,将力转化为顶点的速度
/// </summary>
/// <param name="i"></param>
/// <param name="point"></param>
/// <param name="force"></param>
void AddForceToVertex(int i, Vector3 point, float force)
{
}
网格变形是因为对其每个顶点施加了力。当顶点被推时,它们会获得速度。随着时间的推移,顶点都会改变它们的位置。如果所有顶点都受到完全相同的力,则整个对象将移动而不改变其形状,所以我们需要知道每个顶点的变形力的方向和距离,两者都可以从指向力点到顶点位置的向量中导出。使用平方反比定律找到衰减的力,只需将原始力除以距离的平方,就可以得到衰减的力。
/// <summary>
/// 给某个顶点添加力,将力转化为顶点的速度
/// </summary>
/// <param name="i"></param>
/// <param name="point"></param>
/// <param name="force"></param>
void AddForceToVertex(int i, Vector3 point, float force)
{
//计算施加的力
Vector3 pointToVertex = displacedVertices[i] - point;
//实际上,如果只用F/d*d,再d=0时,衰减的力会变成无穷大,所以除以 1 加上距离的平方,保证了当距离为零时力处于全强度状态。
float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
float velocity = attenuatedForce * Time.deltaTime;
//a = F/m,忽略每个质点的质量,将质量都设为1,则dv = Fdt
vertexVelocities[i] += pointToVertex.normalized * velocity;
}
计算了每个顶点的速度接下来,在MeshDeformer脚本的Update方法中移动顶点
void Update()
{
for (int i = 0; i < displacedVertices.Length; i++)
{
UpdateVertex(i);
}
deformingMesh.vertices = displacedVertices;
deformingMesh.RecalculateNormals();
}
/// <summary>
/// 更新顶点
/// </summary>
/// <param name="i"></param>
void UpdateVertex(int i)
{
Vector3 velocity = vertexVelocities[i];
displacedVertices[i] += velocity * Time.deltaTime;
}
一切顺利会得到以下效果:
增加弹力和阻尼:在MeshFormer.cs中的UpdateVertex中增加弹力和阻尼的计算
/// <summary>
/// 更新顶点
/// </summary>
/// <param name="i"></param>
void UpdateVertex(int i)
{
Vector3 velocity = vertexVelocities[i];
//胡克定律 F = -kx,k是常数,是物体的劲度系数(倔强系数)(弹性系数)x是弹簧的伸长量(或压缩量)
//x = displacedVertices[i] - originalVertices[i]
//F = -springForce * displacement;
Vector3 displacement = displacedVertices[i] - originalVertices[i];
velocity -= displacement * springForce * Time.deltaTime;
vertexVelocities[i] = velocity;
//通过不断减慢顶点的速度来防止这种永恒的振荡。此阻尼效果可替代阻力、阻力、惯性等
//阻尼越高,对象的弹性就越小,看起来越迟缓。
//v = velocity(1-damping)
velocity *= 1f - damping * Time.deltaTime;
displacedVertices[i] += velocity * Time.deltaTime;
}
最后处理:
现在的变形体是放在原点的,而变形体的顶点坐标都是模型坐标系的本地坐标,我们通过射线碰碰撞得到的着力点是在世界坐标系下,因此我们需要将二者变换到同一坐标系下进行力的计算。
/// <summary>
/// 给形变物体施加力
/// </summary>
/// <param name="point"></param>
/// <param name="force"></param>
public void AddDeformingForce(Vector3 point, float force)
{
point = transform.InverseTransformPoint(point);
Debug.DrawLine(Camera.main.transform.position, point);
for (int i = 0; i < displacedVertices.Length; i++)
{
AddForceToVertex(i, point, force);
}
}
物体放缩后,顶点之间的距离会相应的变大或者缩小,如下图:一个球体没有放大和放大两倍的时候的顶点位置。
由上在变形体放缩后需要调整一下每两个顶点之间的作用力,否则用平方反比的计算出来的力,在不同的放缩下,大小会有不同,因此,需要变化三个地方,一个是施加在顶点上的力需要放缩,一个是相互作用力的距离计算时需要放缩,最后一个是顶点移动的距离需要放缩。
最终效果:
完整代码:
public class MeshDeformerInput : MonoBehaviour
{
//施加的力
public float force = 10f;
//力的偏移
public float forceOffset = 0.1f;
void Update()
{
if (Input.GetMouseButton(0))
{
HandleInput();
}
}
void HandleInput()
{
//获得从相机位置往鼠标点击屏幕点方向的射线
Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(inputRay, out hit))
{
MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();
Debug.Log(deformer);
if (deformer)
{
Vector3 point = hit.point;
point += hit.normal * forceOffset;
deformer.AddDeformingForce(point, force);
}
}
}
}
public class MeshDeformer : MonoBehaviour
{
//需要变形的mesh网格
Mesh deformingMesh;
//顶点原始位置,移动后的顶点位置
Vector3[] originalVertices, displacedVertices;
//顶点的速度
Vector3[] vertexVelocities;
//弹力
public float springForce = 20f;
//阻尼
public float damping = 5f;
//放缩比例
float uniformScale = 1f;
void Start()
{
//获取变形网格
deformingMesh = GetComponent<MeshFilter>().mesh;
//获取变形网格的所有顶点位置
originalVertices = deformingMesh.vertices;
displacedVertices = new Vector3[originalVertices.Length];
for (int i = 0; i < originalVertices.Length; i++)
{
displacedVertices[i] = originalVertices[i];
}
vertexVelocities = new Vector3[originalVertices.Length];
}
/// <summary>
/// 给形变物体施加力
/// </summary>
/// <param name="point"></param>
/// <param name="force"></param>
public void AddDeformingForce(Vector3 point, float force)
{
point = transform.InverseTransformPoint(point);
Debug.DrawLine(Camera.main.transform.position, point);
for (int i = 0; i < displacedVertices.Length; i++)
{
AddForceToVertex(i, point, force);
}
}
/// <summary>
/// 给某个顶点添加力,将力转化为顶点的速度
/// </summary>
/// <param name="i"></param>
/// <param name="point"></param>
/// <param name="force"></param>
void AddForceToVertex(int i, Vector3 point, float force)
{
//计算施加力的方向
Vector3 pointToVertex = displacedVertices[i] - point;
pointToVertex *= uniformScale;
//实际上,如果只用F/d*d,再d=0时,衰减的力会变成无穷大,所以除以 1 加上距离的平方,保证了当距离为零时力处于全强度状态。
float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
float velocity = attenuatedForce * Time.deltaTime;
//a = F/m,忽略每个质点的质量,将质量都设为1,则dv = Fdt
vertexVelocities[i] += pointToVertex.normalized * velocity;
}
void Update()
{
uniformScale = this.transform.localScale.x;
for (int i = 0; i < displacedVertices.Length; i++)
{
UpdateVertex(i);
}
deformingMesh.vertices = displacedVertices;
deformingMesh.RecalculateNormals();
}
/// <summary>
/// 更新顶点
/// </summary>
/// <param name="i"></param>
void UpdateVertex(int i)
{
Vector3 velocity = vertexVelocities[i];
//胡克定律 F = -kx,k是常数,是物体的劲度系数(倔强系数)(弹性系数)x是弹簧的伸长量(或压缩量)
//x = displacedVertices[i] - originalVertices[i]
//F = -springForce * displacement;
Vector3 displacement = displacedVertices[i] - originalVertices[i];
displacement *= uniformScale;
velocity -= displacement * springForce * Time.deltaTime;
vertexVelocities[i] = velocity;
//通过不断减慢顶点的速度来防止这种永恒的振荡。此阻尼效果可替代阻力、阻力、惯性等
//阻尼越高,对象的弹性就越小,看起来越迟缓。
//v = velocity(1-damping)
velocity *= 1f - damping * Time.deltaTime;
displacedVertices[i] += velocity * Time.deltaTime / uniformScale;
}
}
参考链接:
网格变形,Unity C# 教程 (catlikecoding.com)