Unity3D 关于模型变形技术代码实现

原创 2017年09月06日 15:31:47

本篇博客给读者介绍关于如何实现模型的变形,在项目开发中经常会涉及到模型的变形操作,比如如下效果图:

第一部分准备工作

首先在Unity中建立一个场景,在场景中放置一个球体,这个球体可以使用Max工具建立,在球体上放几张Materials,效果如下所示:


下面开始具体实现,创建一个新的MeshDeformer脚本来处理变形, 就像立方体球体组件一样,它需要一个网格过滤器来处理。

using UnityEngine;

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

将新组件脚本添加到球体上

接下来要读取网格数据,要进行任何变形,我们需要访问网格。 一旦我们有了网格,我们可以提取原始的顶点位置, 我们还必须在变形期间跟踪位移的顶点。

	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中的执行顺序, 您还可以调整脚本执行顺序,以强制执行谁是第一个和最后一个。

另外,顶点随着网格变形而移动, 所以我们也要存储每个顶点的速度。

	Vector3[] vertexVelocities;

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

现在我们有支持网格变形的基本成分代码下载地址:链接:http://pan.baidu.com/s/1bP4qi6  密码:h1h5

   

第二部分网格变形输入

我们需要一些方法来控制网格如何变形, 我们将使用用户输入,因此是交互式的, 每当用户触摸我们的对象时,我们将在这一点施加力量。

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

如果我们射线击中了一些东西,那东西有一个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);
	}


当网状物体被用户戳戳和凹陷。 这要求接触点附近的顶点被推入表面。 然而,变形力没有固有的方向。 它将在各个方向均匀地应用。 这将导致平坦表面上的顶点被推开,而不是向内推。

我们可以通过将力点拉离表面来增加方向,稍微偏移已经确保顶点总是被推入表面, 接触点的法线可以用作偏移方向。

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



第三部分基本变形

接下来,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;
	}

现在可以使用反平方律找到衰减力 将原始力除以距离平方

Fv=Fd2.其实,我除以一加一个平方的距离:


Fv=F1+d2这保证当距离为零时,力处于全强度。 否则,力将在一个距离处的全强度,并且朝着无限远射击越接近点。



Vector3 pointToVertex = displacedVertices[i] - point;
		float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);

 实际上,力首先被转换为加速度通道:


a=Fm.然后发现速度的变化:Δ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;
	}



我们的顶点现在可以抵抗变形并跳回到原来的位置。 但是他们超越并保持反弹,没有结束。 发生这种情况是因为弹簧

在顶点自身修正时保持拉动,从而提高其速度, 它回落太远后才减速。

我们可以通过不断降低顶点来防止这种永恒的振荡这种阻尼效果是电阻,阻力,惯性等的替代。 这是一个随着时间

的推移降低速度的简单因素
vd=v(1-dΔ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;
	}




我们的网格变形现在完全正常,除非我们转换对象。 我们的所有计算都在本地执行。 继续前进,移动或旋转我们的球体。 您将看到变形力将不正确地应用。
我们必须补偿对象的转换, 我们通过将变形力从世界空间的位置转换为局部空间来实现。

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

现在通过使用统一比例缩放pointToVertex向量来修复AddForceToVertex, 这确保我们使用正确的距离。

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

最后,在任何位置,旋转和均匀刻度上工作的变形网格, 请记住,这是一个简单而相对便宜的视觉效果。 它不是一个软体物理模拟。 物体的碰撞物不改变,所以物理引擎不知道对象的感知形状。



代码下载地址:链接:http://pan.baidu.com/s/1bP4qi6  密码:h1h5






版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Docker_入门?只要这篇就够了!(纯干货适合0基础小白)

与sgy一起开启你的Docker之路 关键词: Docker; mac; Docker中使用gdb无法进入断点,无法调试; 写在前面 这篇博客适合谁? 对于Docker并不了解,只是有一点模糊的...

想要清肠排毒,就喝汁己青汁!

保持肠胃畅通,对于我们的身体是非常重要的。积累毒素会影响我们气色和皮肤。只有身体排毒好了,才能让身体更加轻盈人也更加精神。 日常多加注意一些小细节可帮助你减少毒素积累。 多喝水 早晨最好空腹喝水...

史上最简单的 MySQL 教程(三)「 MySQL 数据库」

MySQL 数据库MySQL 数据库是一种C\S结构的软件,即分为:客户端和服务端。若想访问服务器,必须通过客户端;服务器应该一直运行,客户端则在需要使用的时候运行。

Node.js开发入门—使用对话框ngDialog

做网站经常会遇到弹出对话框获取用户输入或弹出对话框让用户确认某个操作之类的情景,基于AngularJS的扩展模块ngDialog可以帮我们优雅地完成这类事情。
  • foruok
  • foruok
  • 2015-09-06 07:15
  • 13282

ACM竞赛路上亲爱的坑们

写在前边:这些梗都是敝人自己做题和比赛时曾经坑过自己的地方,特别在这里记录一下,所有的链接都是本博客中的题解链接(有大致题意说明和代码),原题请到OJ上自行寻找。目的是提升自身姿势。欢迎大佬们给我提出...

经验分享-前端与后端的接口、HTML分离

在WEB项目中 前后端不分离多人开放效率还不及一个人开发效率来的高,今天分享一个概念

java实现二维码([带]logo)的绘制和解析(zxing by google)

二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础...

Java基础之(三十七)Java多线程编程<二>

控制线程join线程Thread提供了一个线程等待另一个线程完成的方法:join方法。当在某个程序执行流中调用其他线程的join方法,调用join方法的那个线程将被阻塞,直到被join方法加入的joi...

std::map 如何使用结构体作为自定义键值

在使用map时,有时候我们需要自定义键值,才能符合程序的需要。 比如我们需要使用自定义的结构体来作为map的键值.
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)