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

本教程討論了 Unity 中的埃爾米特曲線(更準確地說是三次埃爾米特曲線)和 Catmull-Rom 樣條曲線。後者是一種特殊的三次埃爾米特樣條曲線。由於所有程式碼都在 C# 中實現,因此不需要著色器程式設計。
一些樣條曲線(例如在“貝塞爾曲線”部分討論的二次貝塞爾樣條曲線)不會穿過所有控制點,即它們不會在它們之間插值。另一方面,埃爾米特樣條曲線可以定義為穿過所有控制點。這在許多應用中都是一個有用的特性,例如在動畫中,通常需要為特定的關鍵幀設定特定值,並讓工具為中間幀平滑地插值其他值。

一條三次埃爾米特曲線 ,對於從 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),並將指令碼附加到它(將指令碼從專案視窗拖放到層次結構視窗中的空遊戲物件上)。
然後建立另外四個空遊戲物件(或任何其他遊戲物件),它們具有不同的位置,將用作控制點。選擇帶有指令碼的遊戲物件,並將其他遊戲物件拖放到檢查器中的Start、StartTangentPoint(用於從起點開始的切線的終點)、End和EndTangentPoint插槽中。這將從指定為“Start”的遊戲物件渲染到指定為“End”的遊戲物件的Hermite曲線。

三次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]如果你想了解更多
- 關於埃爾米特樣條曲線,維基百科關於 “三次埃爾米特樣條曲線” 的文章提供了一個很好的起點。
- 關於 Unity 的 LineRenderer,你應該閱讀 Unity 關於該類 LineRenderer 的文件。