跳轉到內容

Cg 程式設計/Unity/埃爾米特曲線

來自華夏公益教科書,開放的世界,開放的書籍
一個埃爾米特樣條曲線,它在控制點對之間平滑插值。

本教程討論了 Unity 中的埃爾米特曲線(更準確地說是三次埃爾米特曲線)和 Catmull-Rom 樣條曲線。後者是一種特殊的三次埃爾米特樣條曲線。由於所有程式碼都在 C# 中實現,因此不需要著色器程式設計。

一些樣條曲線(例如在“貝塞爾曲線”部分討論的二次貝塞爾樣條曲線)不會穿過所有控制點,即它們不會在它們之間插值。另一方面,埃爾米特樣條曲線可以定義為穿過所有控制點。這在許多應用中都是一個有用的特性,例如在動畫中,通常需要為特定的關鍵幀設定特定值,並讓工具為中間幀平滑地插值其他值。

兩條埃爾米特曲線。第一條曲線從 p0 到 p1,在 p0 處切線為 m0,在 p1 處切線為 m1。

埃爾米特曲線

[編輯 | 編輯原始碼]

一條三次埃爾米特曲線 ,對於從 0 到 1 的 ,由起點 和切線 以及終點 和切線 定義。

曲線從(對於 開始,朝向 的方向變化,然後改變方向,朝向 方向移動,對於 ,到達 。如圖所示,透過為對應端點選擇相同的切線向量,可以將兩條埃爾米特曲線平滑地連線在一起。

曲線指令碼

[編輯 | 編輯原始碼]

為了在 Unity 中實現這樣的曲線,我們可以使用 Unity 元件LineRenderer。除了設定一些引數外,還應使用 SetVertexCount 函式設定曲線上取樣點的數量。然後,必須計算取樣點,並使用 SetPosition 函式設定它們。這可以透過這種方式實現

float t;
Vector3 position;

for(int i = 0; i < numberOfPoints; i++)
{
	t = i / (numberOfPoints - 1.0f);
	position = (2.0f * t * t * t - 3.0f * t * t + 1.0f) * p0 
		+ (t * t * t - 2.0f * t * t + t) * m0 
		+ (-2.0f * t * t * t + 3.0f * t * t) * p1 
		+ (t * t * t - t * t) * m1;
	lineRenderer.SetPosition(i, position);
}

這裡,我們使用索引 i 從 0 到 numberOfPoints-1 來計數取樣點。從該索引 i 計算出從 0 到 1 的引數 t。下一行計算 ,然後使用 SetPosition 函式進行設定。

其餘程式碼只是設定了 LineRenderer 元件,並定義了可用於定義控制點和曲線的一些渲染功能的公共變數。

using UnityEngine;

[ExecuteInEditMode, RequireComponent(typeof(LineRenderer))]
public class Hermite_Curve : MonoBehaviour 
{
	public GameObject start, startTangentPoint, end, endTangentPoint;

	public Color color = Color.white;
	public float width = 0.2f;
	public int numberOfPoints = 20;
	LineRenderer lineRenderer;
	
	void Start () 
	{
		lineRenderer = GetComponent<LineRenderer>();
		lineRenderer.useWorldSpace = true;
		lineRenderer.material = new Material(
			Shader.Find("Legacy Shaders/Particles/Additive"));
	}
	
	void Update () 
	{
		// check parameters and components
   		if (null == lineRenderer || null == start || null == startTangentPoint 
			|| null == end || null == endTangentPoint)
   		{
      			return; // no points specified
   		} 

		// update line renderer
		lineRenderer.startColor = color;
		lineRenderer.endColor = color;
   		lineRenderer.startWidth = width;
		lineRenderer.endWidth = width;
		if (numberOfPoints > 0)
   		{
      			lineRenderer.positionCount = numberOfPoints;
   		}

		// set points of Hermite curve
		Vector3 p0 = start.transform.position;
		Vector3 p1 = end.transform.position;
		Vector3 m0 = startTangentPoint.transform.position - start.transform.position;
		Vector3 m1 = endTangentPoint.transform.position - end.transform.position;
		float t;
		Vector3 position;

		for(int i = 0; i < numberOfPoints; i++)
		{
			t = i / (numberOfPoints - 1.0f);
      			position = (2.0f * t * t * t - 3.0f * t * t + 1.0f) * p0 
				+ (t * t * t - 2.0f * t * t + t) * m0 
				+ (-2.0f * t * t * t + 3.0f * t * t) * p1 
				+ (t * t * t - t * t) * m1;
      			lineRenderer.SetPosition(i, position);
		}
	}
}

要使用此指令碼,請在專案視窗建立一個C#指令碼,並將其命名為Hermite_Curve,雙擊它,複製並貼上上面的程式碼,儲存它,建立一個新的空遊戲物件(在主選單中:GameObject > Create Empty),並將指令碼附加到它(將指令碼從專案視窗拖放到層次結構視窗中的空遊戲物件上)。

然後建立另外四個空遊戲物件(或任何其他遊戲物件),它們具有不同的位置,將用作控制點。選擇帶有指令碼的遊戲物件,並將其他遊戲物件拖放到檢查器中的StartStartTangentPoint(用於從起點開始的切線的終點)、EndEndTangentPoint插槽中。這將從指定為“Start”的遊戲物件渲染到指定為“End”的遊戲物件的Hermite曲線。

Catmull-Rom樣條曲線中切線的計算。

Catmull-Rom樣條曲線

[編輯 | 編輯原始碼]

三次Hermite樣條曲線由連續的、平滑的三次Hermite曲線序列組成。為了保證平滑性,一條Hermite曲線的終點的切線與下一條Hermite曲線的起點的切線相同。在某些情況下,使用者提供這些切線(每個控制點一個),而在其他情況下,則需要計算合適的切線。

計算第k個控制點 的切向量 的一種特定方法是:

以及 對於第一個點,以及 對於最後一個點。得到的Hermite三次樣條曲線稱為Catmull-Rom樣條曲線。

樣條曲線指令碼

[編輯 | 編輯原始碼]

以下指令碼實現了這個想法。對於第 j 段,它計算 作為第 j 個控制點 設定為 設定為 (除非它是第一個控制點的切線,在這種情況下它被設定為 )並且 設定為 (除非它是最後一個控制點的切線,那麼它被設定為 )。

p0 = controlPoints[j].transform.position;
p1 = controlPoints[j + 1].transform.position;

if (j > 0) 
{
	m0 = 0.5f * (controlPoints[j + 1].transform.position 
		- controlPoints[j - 1].transform.position);
}
else
{
	m0 = controlPoints[j + 1].transform.position 
		- controlPoints[j].transform.position;
}
if (j < controlPoints.Count - 2)
{
	m1 = 0.5f * (controlPoints[j + 2].transform.position 
		- controlPoints[j].transform.position);
}
else
{
	m1 = controlPoints[j + 1].transform.position 
		- controlPoints[j].transform.position;
}

然後,每段只是作為三次埃爾米特曲線計算。唯一的調整是除最後一段外,所有其他段不應到達 。如果它們到達了,下一段的第一個樣本位置將在同一個位置,這將在渲染中可見。完整的指令碼是

using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode, RequireComponent(typeof(LineRenderer))]
public class Hermite_Spline : MonoBehaviour 
{
	public List<GameObject> controlPoints = new List<GameObject>();
	public Color color = Color.white;
	public float width = 0.2f;
	public int numberOfPoints = 20;
	LineRenderer lineRenderer;	

	void Start () 
	{
		lineRenderer = GetComponent<LineRenderer>();
		lineRenderer.useWorldSpace = true;
		lineRenderer.material = new Material(
			Shader.Find("Legacy Shaders/Particles/Additive"));	
	}
	
	void Update () 
	{
		if (null == lineRenderer || controlPoints == null 
			|| controlPoints.Count < 2)
   		{
      			return; // not enough points specified
   		}

		// update line renderer
		lineRenderer.startColor = color;
		lineRenderer.endColor = color;
   		lineRenderer.startWidth = width;
		lineRenderer.endWidth = width;
		if (numberOfPoints < 2)
   		{
      			numberOfPoints = 2;
   		}
		lineRenderer.positionCount = numberOfPoints * (controlPoints.Count - 1);

		// loop over segments of spline
		Vector3 p0, p1, m0, m1;

		for(int j = 0; j < controlPoints.Count - 1; j++)
		{
			// check control points
			if (controlPoints[j] == null || 
				controlPoints[j + 1] == null ||
				(j > 0 && controlPoints[j - 1] == null) ||
				(j < controlPoints.Count - 2 && controlPoints[j + 2] == null))
			{
				return;  
			}
			// determine control points of segment
			p0 = controlPoints[j].transform.position;
			p1 = controlPoints[j + 1].transform.position;
			
			if (j > 0) 
			{
				m0 = 0.5f * (controlPoints[j + 1].transform.position 
				- controlPoints[j - 1].transform.position);
			}
			else
			{
				m0 = controlPoints[j + 1].transform.position 
					- controlPoints[j].transform.position;
			}
			if (j < controlPoints.Count - 2)
			{
				m1 = 0.5f * (controlPoints[j + 2].transform.position 
					- controlPoints[j].transform.position);
			}
			else
			{
				m1 = controlPoints[j + 1].transform.position 
					- controlPoints[j].transform.position;
			}

			// set points of Hermite curve
			Vector3 position;
			float t;
			float pointStep = 1.0f / numberOfPoints;

			if (j == controlPoints.Count - 2)
			{
				pointStep = 1.0f / (numberOfPoints - 1.0f);
				// last point of last segment should reach p1
			}  
			for(int i = 0; i < numberOfPoints; i++) 
			{
				t = i * pointStep;
				position = (2.0f * t * t * t - 3.0f * t * t + 1.0f) * p0 
					+ (t * t * t - 2.0f * t * t + t) * m0 
					+ (-2.0f * t * t * t + 3.0f * t * t) * p1 
					+ (t * t * t - t * t) * m1;
				lineRenderer.SetPosition(i + j * numberOfPoints, 
					position);
			}
		}
	}
}

該指令碼應命名為 Hermite_Spline,其工作方式與埃爾米特曲線指令碼相同,只是使用者可以指定任意數量的控制點,而不必指定切線點。

總結

[edit | edit source]

在本教程中,我們已經瞭解了

  • 三次埃爾米特曲線的定義和 Catmull-Rom 樣條曲線
  • 使用 Unity 的 LineRenderer 元件實現三次埃爾米特曲線和 Catmull-Rom 樣條曲線。

進一步閱讀

[edit | edit source]

如果你想了解更多

< Cg Programming/Unity

除非另有說明,本頁上的所有示例原始碼均歸屬公共領域。
華夏公益教科書