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 的文档。