跳转到内容

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

除非另有说明,本页上的所有示例源代码均归属公共领域。
华夏公益教科书