Unity——网格变形(制作一个压力球)

主要参考链接:Mesh Deformation, a Unity C# Tutorial(本文为其翻译版)

unity项目下载链接:https://download.csdn.net/download/weixin_43042683/87679832

  • 在物体上投射射线并画出调试线。
  • 将力转换为顶点的速度。
  • 用弹簧和阻尼保持形状。
  • 补偿物体变形。

本教程是一个关于网格变形的介绍。我们将把一个网格变成有弹性的质量,并对其进行戳穿。它适用于Unity 5.0.1及以上版本。

1. 场景设置

我们从一个场景开始,这个场景的中心有一个单一的立方体球体对象。

为了得到一个平滑的变形,球体应该包含相当数量的顶点。把球体的网格大小设置为20,半径为1。

从一个普通的立方体球体开始

2. 网格变形器

创建一个新的MeshDeformer脚本来处理变形问题。就像立方体球体组件一样,它需要一个网格过滤器来工作。

using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
public class MeshDeformer : MonoBehaviour {
}

将新的组件添加到立方体球体中。

带有网格变形器组件的立方体球体

 请注意,我们只需要一个网格过滤器。我们并不关心它是如何得到一个网格的。现在我们使用的是程序化的立方体球体,但它可以是任何网格。

2.1 准备工作

要进行变形,我们需要访问网格。一旦我们有了网格,我们就可以提取原始顶点的位置。在变形过程中,我们还需要跟踪位移的顶点。

	Mesh deformingMesh;
	Vector3[] originalVertices, displacedVertices;

在Start方法中获取网格及其顶点,并将原始顶点复制到位移顶点上。

	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];
		}
	}

我们使用Start,所以程序性网格可以在Awake中生成,Awake总是被首先调用的。这种方法依赖于其他组件在Awake中处理他们的事情,所以它不能保证一定被首先调用。你也可以调整脚本的执行顺序来强制执行谁先谁后。

2.2 顶点速度

顶点会随着网格的变形而移动。所以我们也必须存储每个顶点的速度。

	Vector3[] vertexVelocities;

	void Start () {
		…
		vertexVelocities = new Vector3[originalVertices.Length];
	}

现在我们有了支持网格变形的基本要素。

3. 网格变形器的输入

我们需要一些方法来控制网格的变形方式。我们将使用用户的输入,所以它是互动的。每当用户接触到我们的物体时,我们将在该点施加一个力。

MeshDeformer组件负责实际的变形,但它并不关心输入方法。我们应该创建一个单独的组件来处理用户输入的问题。给它一个可配置的输入力。

using UnityEngine;

public class MeshDeformerInput : MonoBehaviour {

	public float force = 10f;
}

把这个组件附加到摄像机上是最合理的,因为它代表了用户的视角。我们不应该把它附加到变形网格对象上,因为场景中可能有多个变形网格。

网格变形器输入连接到相机

3.1  检测输入

只要默认的鼠标按钮被按住,我们就会处理用户的输入。因此,只要有点击或拖动,就认为用户一直按着方形球。

	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<MeshDeformer>();
		}

Physics.Raycast是如何工作的?
Physics.Raycast是一个静态的方法,用于将射线投射到3D场景中。它有各种不同的变体。最简单的版本有一个射线参数,并返回它是否击中了什么。
我们所使用的版本有一个额外的参数。它是一个类型为RaycastHit的输出参数。这是一个结构,包含关于被击中的东西和接触点的信息。

3.2 施加力

 如果我们撞到了什么东西,而那个东西有一个MeshDeformer组件,那么我们就可以对那个东西进行变形!所以请继续在接触点添加一个变形力。

			MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();
			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);
	}
在场景视图中调试光线

我在哪里可以看到调试线?
它显示在场景视图中,所以在游戏模式下,你必须保持游戏视图和场景视图都是可见的。 

3.3 力量偏移

我们试图唤起的体验是,网格被用户捅破了,凹陷了。这就要求靠近接触点的顶点被推到表面。然而,这个变形力并没有一个固有的方向。它将在所有方向上平等地施加。这将导致平面上的顶点被推开,而不是被推入。

我们可以通过把力点从曲面上拉开来增加一个方向。一个轻微的偏移已经保证了顶点总是被推入曲面。接触点的法线可以作为偏移方向。 

用偏移量改变力的方向。
	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<MeshDeformer>();
			if (deformer) {
				Vector3 point = hit.point;
				point += hit.normal * forceOffset;
				deformer.AddDeformingForce(point, force);
			}
		}
	}
力点稍稍远离物体

4. 基本变形

现在是时候做一些真正的位移了。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) {
	}

4.1 将力转换为速度

网格之所以变形,是因为每个顶点都受到了力的作用。当顶点被推动时,它们会获得一个速度。随着时间的推移,这些顶点都会改变它们的位置。如果所有顶点都经历完全相同的力,整个物体就会移动而不改变其形状。

想想看,一个大爆炸。如果你在地面上,你就会死。如果你在附近,你会被击倒。如果你在远处,就没有问题。力量随着距离的增加而减弱。结合方向上的差异,这种衰减是造成我们物体变形的原因。

所以我们需要知道每个顶点的变形力的方向和距离。两者都可以从一个从力点指向顶点位置的矢量中得到。

	void AddForceToVertex (int i, Vector3 point, float force) {
		Vector3 pointToVertex = displacedVertices[i] - point;
	}

现在可以用反平方定律找到衰减的力。只要用原力除以距离的平方就可以了,F_{v}=\frac{F}{d^{2}} 。实际上,我除以1加上距离的平方,F_{v}=\frac{F}{d^{2}+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;

4.2 移动顶点

现在,顶点有了速度,我们可以移动它们。添加一个更新方法来处理每个顶点。之后,将位移顶点分配给网格,使其实际发生变化。因为网格的形状不再是恒定的,我们也必须重新计算它的法线。

	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;
	}

顶点是否一直在更新?
是的,每次更新时,所有顶点都会被移位,分配给网格,法线也会重新计算。即使在没有施加任何力的情况下。如果用户没有对网格进行变形,那么可以认为是在浪费时间。所以只有在需要不断地使网格变形的时候才使用这个功能。

累积速度

 5. 保持体形

现在,只要我们对它们施加一些力,顶点就开始移动。但它们不会停止。它们继续移动,物体的原始形状就会消失。现在让我们使物体反弹到它的原始形状。

真实的物体是固体,在变形时被压缩和拉伸。它们能抵抗这种变形。一旦不受干扰,它们也能恢复到原来的形状。

我们没有一个真正的体积,只是一个描述表面的顶点集合。我们不能用它来进行现实的物理模拟。但这并不是一个问题。我们真正需要的是看起来可信的东西。

5.1 弹簧

我们同时跟踪每个顶点的原始位置和变形位置。想象一下,我们在每个顶点的两个位置之间附加弹簧。每当变形顶点远离原始顶点时,弹簧会把它拉回来。变形顶点离得越远,弹簧的拉力就越大.

标移位的顶点被拉回题

我们可以直接使用位移矢量作为速度调整,乘以一个可配置的弹簧力。这很简单,看起来也很不错。我们在每次更新顶点的时候都会这样做。

	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;
	}

变形后反弹

5.2 衰减

我们的顶点现在可以抵抗变形并跳回原来的位置。但它们会过冲,一直无休止地跳动。发生这种情况是因为弹簧在顶点自我修正时不断拉动,增加了它的速度。只有在它向后移动太远之后才会减慢速度。

我们可以通过不断减缓顶点的速度来防止这种永恒的振荡。这种阻尼效应可以替代阻力、惯性等等。它是一个简单的因素,随着时间的推移,速度会降低,v_{d}=v(1-d\Delta t)

阻尼越高,物体的弹性就越小,反应也就越迟钝。

	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;
	}

恢复到原来的形状

6. 处理转换

我们的网格变形现在是完全有效的,除了当我们变换物体时。我们所有的计算都是在局部空间进行的。继续前进,移动或旋转我们的球体。你会看到变形力将被错误地应用。

我们必须对物体的变换进行补偿。我们通过将变形力的位置从世界空间转换到本地空间来做到这一点。

	public void AddDeformingForce (Vector3 point, float force) {
		point = transform.InverseTransformPoint(point);
		for (int i = 0; i < displacedVertices.Length; i++) {
			AddForceToVertex(i, point, force);
		}
	}
正确的位置,但比例不同

6.1 调整比例

现在,力被施加在正确的地方,但其他的东西仍然是错误的。将球体均匀地向上或向下缩放。你会注意到,变形的比例是相同的。这是不正确的。小物体和大物体应该受到相同的物理学影响。

 我们必须对我们的物体的比例进行补偿。首先,我们需要知道它的统一尺度。我们可以通过检查变换的一个局部比例轴来找到它。每次更新都要这样做,这样我们就可以在某种程度上处理动态改变比例的对象。

	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);
不同尺度,同样的物理

就这样,你拥有了它。一个可以在任何位置、旋转和统一比例下工作的变形网格。请记住,这是一个简单和相对便宜的视觉效果。它不是一个软体物理模拟。物体的碰撞器并没有改变,所以物理引擎并不知道物体的感知形状。 

猜你喜欢

转载自blog.csdn.net/weixin_43042683/article/details/130088596