(转)Unity3D 之插值计算

时间:2020-11-30 20:22:00

在unity3D中经常用线性插值函数Lerp()来在两者之间插值,两者之间可以是两个材质之间、两个向量之间、两个浮点数之间、两个颜色之间,其函数原型如下:

Material.Lerp 插值

function Lerp (start : Material, end : Material, t : float) : void

在两个材质之间插值

Vector2.Lerp 插值

static function Lerp (from : Vector2, to : Vector2, t : float) : Vector2

两个向量之间的线性插值。按照数字t在form到to之间插值。

t是夹在0到1之间。当t=0时,返回from。当t=1时,返回to。当t=0.5时放回from和to之间的平均数。

Vector3.Lerp 插值

static function Lerp (from : Vector3, to : Vector3, t : float) : Vector3

两个向量之间的线性插值。按照数字t在from到to之间插值。

Vector4.Lerp 插值

static function Lerp (from : Vector4, to : Vector4, t : float) : Vector4

两个向量之间的线形插值。按照数字t在from到to之间插值。t是夹在[0...1]之间的值。,当t = 0时,返回from。当t = 1时,返回to。当t = 0.5 返回from和to的平均数。

Mathf.Lerp 插值

static function Lerp (from : float, to : float, t : float) : float

基于浮点数t返回a到b之间的插值,t限制在0~1之间。当t = 0返回from,当t = 1 返回to。当t = 0.5 返回from和to的平均值。

Color.Lerp 插值

static function Lerp (a : Color, b : Color, t : float) : Color

通过t在颜色a和b之间插值。

"t"是夹在0到1之间的值。当t是0时返回颜色a。当t是1时返回颜色b。

插值,从字面意思上看,就是在其间插入一个数值,这种理解是否正确呢?我们先从最简单的浮点数插值函数来分析:

Mathf.Lerp 插值

static function Lerp (from : float, to : float, t : float) : float

基于浮点数t返回a到b之间的插值,t限制在0~1之间。当t = 0返回from,当t = 1 返回to。当t = 0.5 返回from和to的平均值。

首先,我们来做一个试验,启动Unity3D,任建一个脚本文件,在其Start()中输入内容如下:

void Start () {

print(Mathf.Lerp(0.0f, 100.0f, 0.0f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.1f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.2f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.3f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.4f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.5f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.6f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.7f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.8f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 0.9f).ToString());

print(Mathf.Lerp(0.0f, 100.0f, 1.0f).ToString());

}

运行Unity,在控制台将打印出:

(转)Unity3D 之插值计算

这个实验是在0到100之间插值,插入什么值,取决于第3个参数,从打印结果可看出,第3个参数是个比例因数,是0.1时表示0到100这个长度的十分之一,同理,0.2表示十分之二,依此类推。从这点上看来,我们起初从字面上所理解的插值就是插入一个数值是可以这样理解的。

如果我们把上面那个脚本里的插值函数里的第一个参数变为100.0f,第二个参数变为110.0f,第三个参数保持不变,大家想想其运行结果该是什么呢?可不要认为是0、1、2、3、4、5、6、7、8、9、10了哟,实际结果是100、101、102、103、104、105、106….,因插值是把值插在原来的两数之间,这说明这个函数首先是根据第三个参数所给定的比例算出净增量,再加上起始数,最终算出插值值的。

在Unity3D游戏开发中,应用最多的是Vector3.Lerp 向量插值,下面我们以此插值来猜推其内部实现机理以及一些应用。

(转)Unity3D 之插值计算

如图,在空间中存在两点A(0,10,0)与B(10,0,-10),我们在A、B两点间插入一C点,假设C点的位置在AB的五分之二处,即AC/AB=0.4,根据相似图形对应边成比例的初中几何知识可知,在⊿ABO中AC/AB=OD/OB,同理在⊿OBF中OD/OB=OE/OF,所以AC/AB=OD/O=OE/OF = 0.4,则C点的X坐标值为:OE=0.4*OF=0.4*10=4。

根据上图,还可知ED/FB=0.4,所以C点的Z坐标值DE=0.4*BF=0.4*(-10)=-4。

C点的Y坐标值请看下图:

(转)Unity3D 之插值计算

EO/AO=DF/AF=CB/AC=1-0.4=0.6,则C点的Y坐标值EO=0.6*AO=0.6*10=6。

综上所述,C点的三维坐标为C(4,6,-4)。

下面我们利用Unity3D中的Vector3.Lerp 插值函数:static function Lerp (from : Vector3, to : Vector3, t : float) : Vector3来计算上面演算的插值。

我们把先前脚本中的Start()函数改写成:

void Start()

{ print(Vector3.Lerp(new Vector3(0, 10, 0), new Vector3(10, 0, -10), 0.4f).ToString()); }

其运行结果为:

(转)Unity3D 之插值计算

这与我们的演算结果是一致的。

上面的演算,我们为了简便,A、B两点取得较特殊,降低了演算的复杂度。而对普通的A、B两点,如下图所示:

(转)Unity3D 之插值计算

我们同样可以得到三角形EGL与三角形EFK,使用同样的方法可计算出HI的长度,再加上OH的长度就是C点的X坐标值了。同样的方法可推演出Y与Z的坐标。

手工计算是很复杂的,而Lerp函数可以高效地为我们返回这个插值的,我们在这里做出的演算,只是帮助我们来推测Lerp这个函数的内部实现机理而也,实际运用中,一切工作都是交于Lerp函数去完成。

Lerp函数在游戏开发过程使用较多,在Unity的帮助文档里就有为我们列举了Vector3.Lerp的两个应用的例子,一个是在1秒时间动画位置移动从start.position开始到end.position结束:

using UnityEngine;

using System.Collections;

public class example : MonoBehaviour {

public Transform start;

public Transform end;

void Update() {

transform.position = Vector3.Lerp(start.position, end.position, Time.time);

}

}

另一个例子:

//像弹簧一样跟随目标物体

using UnityEngine;

using System.Collections;

public class example : MonoBehaviour {

public Transform target;

public float smooth = 5.0F;

void Update() {

transform.position = Vector3.Lerp(transform.position, target.position, Time.deltaTime * smooth);

}

}

这个例子中的transform.position是去跟随的那个物体的空间坐标,target.position是目标物体的空间坐标,整句的结果是让跟随物体的坐标不断地变化为它们两者之间的插值,然而随着时间的推移,第三个参数的值最终会为1,所以最终跟随物体的位置会与目标物体重合的。我们以前所玩的游戏中,主人公身上依附着一只宠物如鹰,主人公移动时,鹰会跟随着飞动,主人公移动得快它就飞行跟动得快,始终不会离开主人公,使用Lerp插值函数就可实现。

下面我们来看另一个应用实例。

(转)Unity3D 之插值计算

这是酷跑游戏场景,囚犯沿着一条森林道路向前奔跑,后面有警车追赶,前面有路障,在游戏过程中,我们要在囚犯奔跑的固定路线上随机产生路障,而道路不是平直的,既左右弯曲,又上下起伏,由程序随机生成的路障怎样确定其空间位置呢?这时,Lerp函数就派上了用场。

先根据道路的弯曲与起伏,在转折处设置一个空物体,此空物体的Position值即空间坐标与此处道路一致,我们把这些空物体所在的点称为道路转折点,这些点连接而成的线段所组成的多段折线贴合在路面上,是这条道路的近似路径,这些点取得越多、越准确,这条路径与道路的相似程度就越高。

现在我们用那条路径来代替那条道路,把随机产生的路障放在这条路径上也就是放在道路上了。

假设我们想每隔100米至200米之间产生一个路障,用变量z += Random.Range(100, 200)记录下该路障的Z坐标值(因囚犯总体上是沿着Z轴往前跑)然后根据此Z坐标值判断该坐标值在前面所设置的转折点中的哪两个点之间,找到后就在这两个点之间插值,其插值的比例因数(Lerp()函数的第3个参数)可由两个转折点与这个插值点这三个点中已知的Z坐标值算出来,这样Vector3.Lerp (from : Vector3, to : Vector3, t : float)函数中的三个参数值便都是已知的了,它就可计算出这个插值点的空间坐标了,根据前面的设计,这两个转折点之间的线段是贴合在路面上的,那么此插值的坐标也就是在路面上了,根据此插值放置的路障也就不会偏离道路,且会随着道路的左转而左转,右转而右转,上坡而上坡,下坡而下坡了。

具体设计过程如下。导入道路模型,假设命名为forest_1。模型设计时就确定好了其长度为3000、坐标原点在其终端上了的。导入后我们将其沿Z轴正方向放置在场景中,让其Transorm.Position的X、Y值均为0。我们可以导入多段同类型的道路模型,通过控制它们的Z值来把它们拼接成长长的森林道路。在此道路物体上新建一个空物体作为它的子物体,命名为waypoint,再在其下建立多个为空的孙物体,分别命名为waypoint_01、waypoint_02……,把它们放在道路的转折处,并通过放大、旋转场景图后细调这些孙物体的坐标值,使它们与道路路面贴合,如下图所示:说明:图中的绿色按钮状块就是这些孙物体,因它们是空物体,不能显示在场景中,是通过属性面板(转)Unity3D 之插值计算

给它们设置了一个供编辑时显示使用的图标标示。

(转)Unity3D 之插值计算(转)Unity3D 之插值计算

这样,我们便把弯弯曲曲的道路分成了一段一段的直路段,并记录下来了各段路段两端的特征点的坐标值。有了这些特征点,也就有了与道路相近的路线了。这是化曲为直的方法,把弯曲、起伏的道路化成了与此相近的一段一段的线段。这样的点越多,其相似程度越高。

在waypionts上创建一个脚本组件waypionts.cs:

using UnityEngine;

using System.Collections;

public class waypoints : MonoBehaviour {

public Transform[] points;

void OnDrawGizmos(){

iTween.DrawPath (points);

}

}

public Transform[] points;该句所定义的points就是存放那些特征点的数组,因它是public,可在Unity编辑界面中为其赋值,其操作方法是先在Hierarchy视图中选中waypoints控件,然后在其Inspector视图中点击(转)Unity3D 之插值计算图标锁住其Inspector面板,然后在Hierarchy视图中全选waypoint_01至waypiont_11后拖到属性面板上的数组名points上即可完成赋值,如下图:

(转)Unity3D 之插值计算(转)Unity3D 之插值计算

接下来,在这个森林道路上建立的Forestcs.cs脚本组件里添加生成路障的脚本:

using UnityEngine;

using System.Collections;

public class Forest : MonoBehaviour {

public GameObject[] obstacles;     //路障物体数组

public float startLength = 50;    //路障在道路上出现的开始位置

public float minLength = 100;    //路障距上一个路障的最小距离

public float maxLength = 200;    //路障距上一个路障的最大距离

private Transform player;        //游戏主人公-奔跑者的Transform组件

private waypoints wayPoints;    //与路面相贴合的路线上的脚本组件

void Awake() {

player = GameObject.FindGameObjectWithTag(Tags.player).transform;  //找到游戏主人公-奔跑者并获得它的Transform组件

wayPoints = transform.Find("waypoints").GetComponent<waypoints>();  //找到与路面相贴合的路线上的脚本组件

}

// Use this for initialization

void Start()

{

GenerateObstacle();    //当森林道路被创建出来时,就会自动调用此Start()方法,从而调用此GenerateObstacle()方法

}

// 如果主人公跑完了这段道路,则通知GenerateForest类开始运行产生新的道路,并销毁已跑完的这条道路

void Update () {

if (player.position.z > transform.position.z+100) {

Camera.main.SendMessage("GenerateForest");

GameObject.Destroy(this.gameObject);

}

}

void GenerateObstacle(){

float startZ = transform.position.z - 3000;  //当前道路在场景中的起始Z坐标

float endZ = transform.position.z;          //当前道路在场景中的结束Z坐标

float z = startZ + startLength;             //将要产生的路障的Z坐标

while (true) {

z += Random.Range(100, 200);            //每隔100多米的距离产生一个路障

if (z > endZ)                           //如果将要产生路障的位置超出了这条道路则退出路障产生循环,否则产生路障

{

break;

}

else {

Vector3 position = GetWayPosByz(z);                    //调用GetWayPosByz()方法计算路障位置坐标

int obsIndex = Random.Range(0, obstacles.Length);      //产生一个从路障数组里取路障的随机序数

GameObject.Instantiate(obstacles[obsIndex], position, Quaternion.identity);//实例化路障

}

}

}

Vector3 GetWayPosByz(float z) {

Transform[] points = wayPoints.points;       //在道路上设置的转折点的集合

int index = 0;                               //转折点在集合中的序数号

for (int i = 0; i < points.Length-1; i++) { //根据要插入路障的Z值在集合中寻找在哪两个点之间,找到后记下序数号

if(z<=points[i].position.z && z>= points[i+1].position.z){

index = i;

break;

}

}

//使用Lerp函数计算出插入路障处的空间坐标值

return Vector3.Lerp(points[index + 1].position, points[index].position, (z - points[index + 1].position.z) / (points[index].position.z - points[index + 1].position.z));

}

}