主要参考链接:Mesh Deformation, a Unity C# Tutorial(本文为其翻译版)
unity项目下载链接:https://download.csdn.net/download/weixin_43042683/87679832
本教程是一个关于网格变形的介绍。我们将把一个网格变成有弹性的质量,并对其进行戳穿。它适用于Unity 5.0.1及以上版本。
我们从一个场景开始,这个场景的中心有一个单一的立方体球体对象。
为了得到一个平滑的变形,球体应该包含相当数量的顶点。把球体的网格大小设置为20,半径为1。
创建一个新的MeshDeformer脚本来处理变形问题。就像立方体球体组件一样,它需要一个网格过滤器来工作。
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
public class MeshDeformer : MonoBehaviour {
}
将新的组件添加到立方体球体中。
请注意,我们只需要一个网格过滤器。我们并不关心它是如何得到一个网格的。现在我们使用的是程序化的立方体球体,但它可以是任何网格。
要进行变形,我们需要访问网格。一旦我们有了网格,我们就可以提取原始顶点的位置。在变形过程中,我们还需要跟踪位移的顶点。
Mesh deformingMesh;
Vector3[] originalVertices, displacedVertices;
在Start方法中获取网格及其顶点,并将原始顶点复制到位移顶点上。
void Start () {
deformingMesh = GetComponent().mesh;
originalVertices = deformingMesh.vertices;
displacedVertices = new Vector3[originalVertices.Length];
for (int i = 0; i < originalVertices.Length; i++) {
displacedVertices[i] = originalVertices[i];
}
}
我们使用Start,所以程序性网格可以在Awake中生成,Awake总是被首先调用的。这种方法依赖于其他组件在Awake中处理他们的事情,所以它不能保证一定被首先调用。你也可以调整脚本的执行顺序来强制执行谁先谁后。
顶点会随着网格的变形而移动。所以我们也必须存储每个顶点的速度。
Vector3[] vertexVelocities;
void Start () {
…
vertexVelocities = new Vector3[originalVertices.Length];
}
现在我们有了支持网格变形的基本要素。
我们需要一些方法来控制网格的变形方式。我们将使用用户的输入,所以它是互动的。每当用户接触到我们的物体时,我们将在该点施加一个力。
MeshDeformer组件负责实际的变形,但它并不关心输入方法。我们应该创建一个单独的组件来处理用户输入的问题。给它一个可配置的输入力。
using UnityEngine;
public class MeshDeformerInput : MonoBehaviour {
public float force = 10f;
}
把这个组件附加到摄像机上是最合理的,因为它代表了用户的视角。我们不应该把它附加到变形网格对象上,因为场景中可能有多个变形网格。
只要默认的鼠标按钮被按住,我们就会处理用户的输入。因此,只要有点击或拖动,就认为用户一直按着方形球。
void Update () {
if (Input.GetMouseButton(0)) {
HandleInput();
}
}
现在我们必须弄清楚用户的指向。我们通过从摄像机向场景中投射一条射线来完成这个任务。我们将抓取场景中的主摄像机,并使用它来将光标位置转换为射线。
void HandleInput () {
Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
}
我们使用物理引擎来投射射线并存储它所击中的信息。如果射线撞到了什么东西,我们可以从被撞到的物体中获取MeshDeformer组件。
Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(inputRay, out hit)) {
MeshDeformer deformer = hit.collider.GetComponent();
}
Physics.Raycast是如何工作的?
Physics.Raycast是一个静态的方法,用于将射线投射到3D场景中。它有各种不同的变体。最简单的版本有一个射线参数,并返回它是否击中了什么。
我们所使用的版本有一个额外的参数。它是一个类型为RaycastHit的输出参数。这是一个结构,包含关于被击中的东西和接触点的信息。
如果我们撞到了什么东西,而那个东西有一个MeshDeformer组件,那么我们就可以对那个东西进行变形!所以请继续在接触点添加一个变形力。
MeshDeformer deformer = hit.collider.GetComponent();
if (deformer) {
Vector3 point = hit.point;
deformer.AddDeformingForce(point, force);
}
当然这要假设我们的MeshDeformer组件有一个AddDeformingForce方法。所以要添加这个方法。不过,我们先不要做任何变形。首先,从主摄像机到该点画一条调试线,以使射线可视化。
public void AddDeformingForce (Vector3 point, float force) {
Debug.DrawLine(Camera.main.transform.position, point);
}
我在哪里可以看到调试线?
它显示在场景视图中,所以在游戏模式下,你必须保持游戏视图和场景视图都是可见的。
我们试图唤起的体验是,网格被用户捅破了,凹陷了。这就要求靠近接触点的顶点被推到表面。然而,这个变形力并没有一个固有的方向。它将在所有方向上平等地施加。这将导致平面上的顶点被推开,而不是被推入。
我们可以通过把力点从曲面上拉开来增加一个方向。一个轻微的偏移已经保证了顶点总是被推入曲面。接触点的法线可以作为偏移方向。
public float forceOffset = 0.1f;
void HandleInput () {
Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(inputRay, out hit)) {
MeshDeformer deformer = hit.collider.GetComponent();
if (deformer) {
Vector3 point = hit.point;
point += hit.normal * forceOffset;
deformer.AddDeformingForce(point, force);
}
}
}
现在是时候做一些真正的位移了。MeshDeformer.AddDeformingForce必须循环浏览所有当前位移的顶点,并对每个顶点单独施加变形力。
public void AddDeformingForce (Vector3 point, float force) {
for (int i = 0; i < displacedVertices.Length; i++) {
AddForceToVertex(i, point, force);
}
}
void AddForceToVertex (int i, Vector3 point, float force) {
}
网格之所以变形,是因为每个顶点都受到了力的作用。当顶点被推动时,它们会获得一个速度。随着时间的推移,这些顶点都会改变它们的位置。如果所有顶点都经历完全相同的力,整个物体就会移动而不改变其形状。
想想看,一个大爆炸。如果你在地面上,你就会死。如果你在附近,你会被击倒。如果你在远处,就没有问题。力量随着距离的增加而减弱。结合方向上的差异,这种衰减是造成我们物体变形的原因。
所以我们需要知道每个顶点的变形力的方向和距离。两者都可以从一个从力点指向顶点位置的矢量中得到。
void AddForceToVertex (int i, Vector3 point, float force) {
Vector3 pointToVertex = displacedVertices[i] - point;
}
现在可以用反平方定律找到衰减的力。只要用原力除以距离的平方就可以了, 。实际上,我除以1加上距离的平方,。这就保证了当距离为零时,力是全开的。否则,当距离为1时,力就会处于全盛状态,而当你越接近该点时,它就会向无穷远处射去。
Vector3 pointToVertex = displacedVertices[i] - point;
float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
现在我们有了我们的力,我们可以把它转换为速度Δ。实际上,这个力首先通过以下方式转换为一个加速度a=F/m 那么速度的变化可以通过以下方式找到Δv=aΔt,为了简单起见,我们将忽略质量,就像每个顶点都是一个一样。因此,我们最终会得到Δv=FΔt。
Vector3 pointToVertex = displacedVertices[i] - point;
float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
float velocity = attenuatedForce * Time.deltaTime;
在这一点上,我们有一个速度Δ,但还没有一个方向。我们通过对开始时的矢量进行归一化来找到它。然后我们可以把结果加到顶点速度上。
Vector3 pointToVertex = displacedVertices[i] - point;
float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
float velocity = attenuatedForce * Time.deltaTime;
vertexVelocities[i] += pointToVertex.normalized * velocity;
现在,顶点有了速度,我们可以移动它们。添加一个更新方法来处理每个顶点。之后,将位移顶点分配给网格,使其实际发生变化。因为网格的形状不再是恒定的,我们也必须重新计算它的法线。
void Update () {
for (int i = 0; i < displacedVertices.Length; i++) {
UpdateVertex(i);
}
deformingMesh.vertices = displacedVertices;
deformingMesh.RecalculateNormals();
}
更新一个顶点是一个调整其位置的问题,通过Δp=vΔt。
void UpdateVertex (int i) {
Vector3 velocity = vertexVelocities[i];
displacedVertices[i] += velocity * Time.deltaTime;
}
顶点是否一直在更新?
是的,每次更新时,所有顶点都会被移位,分配给网格,法线也会重新计算。即使在没有施加任何力的情况下。如果用户没有对网格进行变形,那么可以认为是在浪费时间。所以只有在需要不断地使网格变形的时候才使用这个功能。
现在,只要我们对它们施加一些力,顶点就开始移动。但它们不会停止。它们继续移动,物体的原始形状就会消失。现在让我们使物体反弹到它的原始形状。
真实的物体是固体,在变形时被压缩和拉伸。它们能抵抗这种变形。一旦不受干扰,它们也能恢复到原来的形状。
我们没有一个真正的体积,只是一个描述表面的顶点集合。我们不能用它来进行现实的物理模拟。但这并不是一个问题。我们真正需要的是看起来可信的东西。
我们同时跟踪每个顶点的原始位置和变形位置。想象一下,我们在每个顶点的两个位置之间附加弹簧。每当变形顶点远离原始顶点时,弹簧会把它拉回来。变形顶点离得越远,弹簧的拉力就越大.
我们可以直接使用位移矢量作为速度调整,乘以一个可配置的弹簧力。这很简单,看起来也很不错。我们在每次更新顶点的时候都会这样做。
public float springForce = 20f;
void UpdateVertex (int i) {
Vector3 velocity = vertexVelocities[i];
Vector3 displacement = displacedVertices[i] - originalVertices[i];
velocity -= displacement * springForce * Time.deltaTime;
vertexVelocities[i] = velocity;
displacedVertices[i] += velocity * Time.deltaTime;
}
我们的顶点现在可以抵抗变形并跳回原来的位置。但它们会过冲,一直无休止地跳动。发生这种情况是因为弹簧在顶点自我修正时不断拉动,增加了它的速度。只有在它向后移动太远之后才会减慢速度。
我们可以通过不断减缓顶点的速度来防止这种永恒的振荡。这种阻尼效应可以替代阻力、惯性等等。它是一个简单的因素,随着时间的推移,速度会降低,。
阻尼越高,物体的弹性就越小,反应也就越迟钝。
public float damping = 5f;
void UpdateVertex (int i) {
Vector3 velocity = vertexVelocities[i];
Vector3 displacement = displacedVertices[i] - originalVertices[i];
velocity -= displacement * springForce * Time.deltaTime;
velocity *= 1f - damping * Time.deltaTime;
vertexVelocities[i] = velocity;
displacedVertices[i] += velocity * Time.deltaTime;
}
我们的网格变形现在是完全有效的,除了当我们变换物体时。我们所有的计算都是在局部空间进行的。继续前进,移动或旋转我们的球体。你会看到变形力将被错误地应用。
我们必须对物体的变换进行补偿。我们通过将变形力的位置从世界空间转换到本地空间来做到这一点。
public void AddDeformingForce (Vector3 point, float force) {
point = transform.InverseTransformPoint(point);
for (int i = 0; i < displacedVertices.Length; i++) {
AddForceToVertex(i, point, force);
}
}
现在,力被施加在正确的地方,但其他的东西仍然是错误的。将球体均匀地向上或向下缩放。你会注意到,变形的比例是相同的。这是不正确的。小物体和大物体应该受到相同的物理学影响。
我们必须对我们的物体的比例进行补偿。首先,我们需要知道它的统一尺度。我们可以通过检查变换的一个局部比例轴来找到它。每次更新都要这样做,这样我们就可以在某种程度上处理动态改变比例的对象。
float uniformScale = 1f;
void Update () {
uniformScale = transform.localScale.x;
…
}
非均匀比例怎么办?
你可以使用一个三维矢量,而不是一个单一的刻度值。然后分别调整每个维度。但实际上,你并不想处理非均匀比例的问题。
现在固定AddForceToVertex,将pointToVertex向量按统一比例缩放。这可以确保我们使用正确的距离。
void AddForceToVertex (int i, Vector3 point, float force) {
Vector3 pointToVertex = displacedVertices[i] - point;
pointToVertex *= uniformScale;
float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
float velocity = attenuatedForce * Time.deltaTime;
vertexVelocities[i] += pointToVertex.normalized * velocity;
}
在UpdateVertex中对位移做同样的处理。现在我们的速度是正确的。
void UpdateVertex (int i) {
Vector3 velocity = vertexVelocities[i];
Vector3 displacement = displacedVertices[i] - originalVertices[i];
displacement *= uniformScale;
velocity -= displacement * springForce * Time.deltaTime;
velocity *= 1f - damping * Time.deltaTime;
vertexVelocities[i] = velocity;
displacedVertices[i] += velocity * Time.deltaTime;
}
然而,对于一个没有被缩放的物体,我们的速度现在是正确的。由于我们的对象实际上是按比例的,我们也必须调整顶点运动。这一次我们必须用除法而不是用乘法。
displacedVertices[i] += velocity * (Time.deltaTime / uniformScale);
就这样,你拥有了它。一个可以在任何位置、旋转和统一比例下工作的变形网格。请记住,这是一个简单和相对便宜的视觉效果。它不是一个软体物理模拟。物体的碰撞器并没有改变,所以物理引擎并不知道物体的感知形状。