在本篇博客中,我们将探讨如何使用Unity实现3D贝塞尔曲线变形效果。
3D贝塞尔曲线变形
贝塞尔曲线是一种常用的数学曲线,通过控制点和曲线影响力物体我们可以实现对网格的弯曲和变形。
在示例代码中,我们可以看到通过设置控制点数组、曲线影响力物体和控制物体曲线施加力等变量。
实现了对网格进行贝塞尔曲线变形的效果。这种技术在游戏中常用于创建动态的形变效果。
如弯曲的绳索、变形的角色模型等。
贝塞尔曲线基础
贝塞尔曲线是一种由控制点定义的数学曲线。
在3D空间中,我们通常使用3次贝塞尔曲线,它由四个控制点(起始点、两个中间点和结束点)组成。
贝塞尔曲线的形状受控制点的位置和权重影响。
控制点的位置决定了曲线经过的路径,而权重控制了曲线在控制点之间的弯曲程度。
线性公式
给定点P0、P1,线性贝兹曲线只是一条两点之间的直线。
二次方公式
二次方贝兹曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
TrueType字型就运用了以贝兹样条组成的二次贝兹曲线。
三次方公式
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝兹曲线。
曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2。
这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P3之前
走向P2方向的“长度有多长”。
曲线的参数形式为:
现代的成象系统,如PostScript、Asymptote和Metafont
运用了以贝兹样条组成的三次贝兹曲线,用来描绘曲线轮廓。
Unity 实现3D贝塞尔曲线变形
准备工作
首先,我们需要在Unity中创建一个空的GameObject,并将该脚本附加到该GameObject上。
接下来,我们需要创建一个网格,可以通过导入一个3D模型或者使用Unity内置的网格创建工具来完成。
脚本概述
让我们先来看一下脚本的整体结构:
1.变量定义
2.Start()函数
2.Update()函数
3.初始化函数
4.获取所有子节点函数
5.贝塞尔曲线Mesh计算相关函数
变量定义
[Header("存储贝塞尔曲线控制点数组")]
public List<Transform> _CpArr;
[Header("曲线影响力物体")]
public Transform _ForceItem;
[Range(0, 100f)]
[Header("控制物体曲线施加力")]
public float _Force = 10;
[Header("原始顶点位置")]
private Vector3[] _OriVertices;
[Header("网格数据")]
private Mesh _Mesh;
[Header("最后一个控制点位置")]
//最后一个控制点的位置。用来计算mesh高度来计算t
private Vector3 _TopPos;
变量解析
_CpArr: 存储贝塞尔曲线控制点数组的列表。
_ForceItem: 曲线影响力物体的Transform组件。
_Force: 控制物体曲线施加力的大小,范围在0到100之间。
_OriVertices: 原始顶点位置的数组。
_Mesh: 网格数据的Mesh组件。
_TopPos: 最后一个控制点的位置,用于计算mesh高度来计算t。
函数解析 获取所有子节点
Start(): 在脚本启动时调用,用于初始化操作。
Update(): 在每一帧更新时调用,用于更新控制点和网格。
Initialize(): 初始化函数,用于设置控制点数组和作用力变量等。
GetAllChildren 获取所有子节点
GetAllChildren: 获取所有子节点的函数,用于递归收集父节点下的所有子节点。
/// <summary>
/// 获取所有子节点
/// </summary>
/// <param 父节点="_Parent"></param>
/// <returns></returns>
private List<Transform> GetAllChildren(Transform _Parent)
{
List<Transform> _ListTra = new List<Transform>();
foreach (Transform _Child in _Parent)
{
//添加直接子节点
_ListTra.Add(_Child);
//递归收集后代节点
List<Transform> _Descendants = GetAllChildren(_Child);
_ListTra.AddRange(_Descendants);
}
return _ListTra;
}
UpdateBezierBend 控制点更新
UpdateBezierBend: 控制点更新函数,根据施加在曲线上的力,计算并更新控制点的旋转角度。
/// <summary>
/// 控制点更新
/// 根据施加在曲线上的力,计算并更新控制点的旋转角度
/// </summary>
private void UpdateBezierBend()
{
//曲线弯曲方向
Vector3 _BendVector = new Vector3(0, 0, 0);
//弯曲布尔 true 无弯曲 false 弯曲
bool _IsVertical = true;
for (int i = 1; i < _CpArr.Count; i++)
{
//对于每个控制点,将其位置转换为第一个控制点 _CpArr[0] 的局部空间
Vector3 _Pos = _CpArr[i].TransformPoint(new Vector3(0, 0, 0));
_Pos = _CpArr[0].InverseTransformPoint(_Pos);
//判断当前控制点 x z 是否在非零
//如果非零 就证明曲线在一个非垂直方向上弯曲
if (IsEqualZero(_Pos.x) == false || IsEqualZero(_Pos.z) == false)
{
//临时弯曲变量存储
_BendVector.x = _Pos.x;
_BendVector.z = _Pos.z;
//弯曲布尔
_IsVertical = false;
break;
}
}
//原始网格顶点数组
Vector3[] _Temp = (Vector3[])_Mesh.vertices.Clone();
//遍历所有顶点
for (int i = 0; i < _OriVertices.Length; i++)
{
//获取顶点坐标,计算t值
Vector3 _OriPos = _OriVertices[i];
Vector3 _BendPos;
//没有弯曲
if (_IsVertical == true)
{
//当前顶点位置不变
_BendPos = _OriPos;
}
//发生弯曲
else
{
//将顶点的原始位置 _OriPos 的 y 分量除以顶部控制点 _TopPos 的 y 分量,计算参数 _CurvePosition
float _T = _OriPos.y / _TopPos.y;
//获取顶点在贝塞尔曲线上对应的坐标
Vector3 _BezierPos = CalculateBezier(_T);
//获取顶点在曲线上应有的法线偏移向量
Vector3 _NormalVector = GetBendNormalVector(_T, _OriPos, _BendVector);
//获取顶点在曲线上应有的垂直偏移向量
Vector3 _VerticalVector = new Vector3(_OriPos.x, 0, _OriPos.z) - Vector3.Project(new Vector3(_OriPos.x, 0, _OriPos.z), _BendVector);
//获取顶点最终弯曲位置
_BendPos = _BezierPos + _NormalVector + _VerticalVector;
}
//转换回 mesh 本地坐标系
_BendPos = _CpArr[0].TransformPoint(_BendPos);
_BendPos = transform.InverseTransformPoint(_BendPos);
_Temp[i] = _BendPos;
}
_Mesh.vertices = _Temp;
}
CalculateBezier Bezier 曲线公式
CalculateBezier: Bezier曲线公式计算函数,根据给定的t值计算贝塞尔曲线上的点坐标。
/// <summary>
/// Bezier 曲线公式
/// 计算贝塞尔曲线上给定参数值 _CurvePosition 对应的位置
/// </summary>
/// <param 曲线位置="_CurvePosition"></param>
/// <returns></returns>
private Vector3 CalculateBezier(float _CurvePosition)
{
//存储坐标
Vector3 _Ret = new Vector3(0, 0, 0);
//控制点数量
int _Number = _CpArr.Count - 1;
for (int i = 0; i <= _Number; i++)
{
//获取第 i 个控制点的世界坐标,通过将其位置从控制点的局部坐标系转换到世界坐标
Vector3 _Pi = _CpArr[i].TransformPoint(new Vector3(0, 0, 0));
//将转换后的世界坐标再次转换回第一个控制点的局部坐标系,用于与其他控制点进行计算
_Pi = _CpArr[0].InverseTransformPoint(_Pi);
//根据贝塞尔曲线的定义,计算贝塞尔基函数的乘积项,并与控制点的局部坐标相乘
//Cn_m(_Number, i) 是组合数函数 表示从 _Number 个控制点中选择 i 个的组合数
_Ret = _Ret + Mathf.Pow(1 - _CurvePosition, _Number - i) * Mathf.Pow(_CurvePosition, i) * Cn_m(_Number, i) * _Pi;
}
return _Ret;
}
GetBendNormalVector 获取指定点上的法线向量偏移
GetBendNormalVector: 获取顶点在曲线上应有的法线偏移向量的函数。
/// <summary>
/// 获取指定点上的法线向量偏移
/// 计算在贝塞尔曲线上给定参数值 _CurvePosition 对应位置的弯曲法向量
/// </summary>
/// <param 曲线位置="_CurvePosition"></param>
/// <param 原始位置="_OriPos"></param>
/// <param 弯曲方向="_BendVector"></param>
/// <returns></returns>
private Vector3 GetBendNormalVector(float _CurvePosition, Vector3 _OriPos, Vector3 _BendVector)
{
//切线斜率
Vector3 _TangentVector = CalculateBezierTangent(_CurvePosition);
//切线竖直时,顶点在在弯曲向量上的投影向量即为法线向量
if (IsEqualZero(_TangentVector.x) == true && IsEqualZero(_TangentVector.z) == true)
{
//直接返回 原始位置 _OriPos 投影到弯曲向量 _BendVector 上的向量,作为法线向量
return Vector3.Project(new Vector3(_OriPos.x, 0, _OriPos.z), _BendVector);
}
//存储计算得到的法线向量
Vector3 _NormalVector = new Vector3(0, 0, 0);
//计算切线向量与原始位置向量的点乘
float _DirectFlag = Vector3.Dot(_BendVector, _OriPos);
//判断法线向量朝向(法线向量有两个方向)
//大于零 顶点坐标与弯曲方向同向
if (_DirectFlag > 0)
{
//切线水平,法线向量竖直向下
if (IsEqualZero(_TangentVector.y) == true)
{
_NormalVector.y = -1;
}
else
{
//切线朝上,法线向量与切线水平同向
if (_TangentVector.y > 0)
{
_NormalVector.x = _TangentVector.x;
_NormalVector.z = _TangentVector.z;
}
//切线朝下,法线向量与切线水平反向
else
{
_NormalVector.x = -_TangentVector.x;
_NormalVector.z = -_TangentVector.z;
}
//使法线向量与切线向量水平且垂直于切线向量
_NormalVector.y = -(_TangentVector.x * _NormalVector.x + _TangentVector.z * _NormalVector.z) / _TangentVector.y;
}
}
//小于零 顶点坐标与弯曲方向反向
else
{
//切线水平,法线向量竖直向上
if (IsEqualZero(_TangentVector.y) == true)
{
_NormalVector.y = 1;
}
else
{
//切线朝上,法线向量与切线水平反向
if (_TangentVector.y > 0)
{
_NormalVector.x = -_TangentVector.x;
_NormalVector.z = -_TangentVector.z;
}
//切线朝下,法线向量与切线水平同向
else
{
_NormalVector.x = _TangentVector.x;
_NormalVector.z = _TangentVector.z;
}
//使得法线向量与切线向量水平且垂直于切线向量
_NormalVector.y = -(_TangentVector.x * _NormalVector.x + _TangentVector.z * _NormalVector.z) / _TangentVector.y;
}
}
//计算法线向量的模
//法线向量的模应为到投影到弯曲面后,到中心点的距离
float _Magnitude = Vector3.Project(new Vector3(_OriPos.x, 0, _OriPos.z), _BendVector).magnitude;
//将法线向量按照该模进行缩放,使其长度与距离一致
_NormalVector = _NormalVector.normalized * _Magnitude;
//返回法线向量
return _NormalVector;
}
UpdateControlPoint 更新控制点的旋转角度
UpdateControlPoint: 控制点更新函数,根据施加在曲线上的力,计算并更新控制点的位置。
/// <summary>
/// 更新控制点的旋转角度
/// 根据受力计算各个控制点的旋转角度
/// </summary>
private void UpdateControlPoint()
{
//受力强度
float _HandleForce = _Force;
//根据受力计算各个控制点旋转角度
for (int i = 1; i <= _CpArr.Count - 2; i++)
{
//计算最大弯曲方向
Vector3 _ForcePos = _ForceItem.transform.TransformPoint(new Vector3(0, 0, 0));
_ForcePos = _CpArr[i - 1].InverseTransformPoint(_ForcePos);
//计算从控制点到受力位置的向量
Vector3 _ToVector = _ForcePos - _CpArr[i].localPosition;
//得到该控制点的旋转角度
Quaternion _MaxRotation = Quaternion.FromToRotation(Vector3.up, _ToVector);
//获取控制点组件
ControlPoint_ZH _Cp = _CpArr[i].gameObject.GetComponent<ControlPoint_ZH>();
//计算弯曲比例
float _RotateRate = Mathf.Clamp(_HandleForce / _Cp._BendForce, 0f, 1.0f);
//设置旋转角度
_CpArr[i].localRotation = Quaternion.Lerp(Quaternion.Euler(0, 0, 0), _MaxRotation, _RotateRate);
}
}
CalculateBezierTangent 曲线求导(切线向量)
CalculateBezierTangent: 计算贝塞尔曲线上指定t值处的切线向量。
/// <summary>
/// 曲线求导(切线向量)
/// </summary>
/// <param 曲线位置="_CurvePosition"></param>
/// <returns></returns>
private Vector3 CalculateBezierTangent(float _CurvePosition)
{
//存储计算得到的切线向量
Vector3 _Ret = new Vector3(0, 0, 0);
//控制点数量
int _Number = _CpArr.Count - 1;
for (int i = 0; i <= _Number; i++)
{
Vector3 _Pi = _CpArr[i].TransformPoint(new Vector3(0, 0, 0));
_Pi = _CpArr[0].InverseTransformPoint(_Pi);
//根据贝塞尔曲线的定义,计算贝塞尔基函数的导数项,并与控制点的局部坐标相乘
//每次循环迭代,都将计算得到的导数项加到 _Ret 向量上
_Ret = _Ret + (-1 * (_Number - i) * Mathf.Pow(1 - _CurvePosition, _Number - i - 1) * Mathf.Pow(_CurvePosition, i) * Cn_m(_Number, i) * _Pi + i * Mathf.Pow(1 - _CurvePosition, _Number - i) * Mathf.Pow(_CurvePosition, i - 1) * Cn_m(_Number, i) * _Pi);
}
//返回计算得到的切线向量 _Ret,表示贝塞尔曲线上给定参数值 _CurvePosition 对应的切线向量
return _Ret;
}
BesselCurveDeformation_ZH 完整代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
/// <summary>
/// 3D贝塞尔曲线变形
/// </summary>
public class BesselCurveDeformation_ZH : MonoBehaviour
{
[Header("存储贝塞尔曲线控制点数组")]
public List<Transform> _CpArr;
[Header("曲线影响力物体")]
public Transform _ForceItem;
[Range(0, 100f)]
[Header("控制物体曲线施加力")]
public float _Force = 10;
[Header("原始顶点位置")]
private Vector3[] _OriVertices;
[Header("网格数据")]
private Mesh _Mesh;
[Header("最后一个控制点位置")]
//最后一个控制点的位置。用来计算mesh高度来计算t
private Vector3 _TopPos;
void Start()
{
Initialize();
}
void Update()
{
//根据受力,修改控制点
UpdateControlPoint();
//更新mesh
UpdateBezierBend();
}
/// <summary>
/// 初始化
/// </summary>
private void Initialize()
{
//初始化 控制点数组
_CpArr = new List<Transform>();
//if (GameObject.Find("Node1").transform != null)
//{
// //数组清空
// _CpArr.Clear();
// //获取所有控制端点
// _CpArr = GetAllChildren(GameObject.Find("Node").transform);
//}
//else
{
GameObject _EmptyObject0 = new GameObject("P0");
GameObject _EmptyObject1 = new GameObject("P1");
GameObject _EmptyObject2 = new GameObject("P2");
GameObject _EmptyObject3 = new GameObject("P3");
GameObject _EmptyObject4 = new GameObject("P4");
GameObject _EmptyObject5 = new GameObject("P5");
_EmptyObject0.transform.SetParent(transform);
_EmptyObject0.transform.localPosition=new Vector3(GridCreation_ZH._Instance._MeshHeight/2*-1, 0,0);
_EmptyObject1.transform.SetParent(_EmptyObject0.transform);
_EmptyObject1.transform.localPosition = new Vector3(0, GridCreation_ZH._Instance._MeshHeight/6, 0);
_EmptyObject2.transform.SetParent(_EmptyObject1.transform);
_EmptyObject2.transform.localPosition = new Vector3(0, GridCreation_ZH._Instance._MeshHeight / 6, 0);
_EmptyObject3.transform.SetParent(_EmptyObject2.transform);
_EmptyObject3.transform.localPosition = new Vector3(0, GridCreation_ZH._Instance._MeshHeight / 6, 0);
_EmptyObject4.transform.SetParent(_EmptyObject3.transform);
_EmptyObject4.transform.localPosition = new Vector3(0, GridCreation_ZH._Instance._MeshHeight / 6, 0);
_EmptyObject5.transform.position = new Vector3(0, GridCreation_ZH._Instance._MeshHeight, 0);
//_EmptyObject5.transform.eulerAngles=new Vector3(0, 0, 90);
_EmptyObject5.transform.SetParent(_EmptyObject4.transform);
_CpArr.Add(_EmptyObject0.transform);
_CpArr.Add(_EmptyObject1.transform);
_CpArr.Add(_EmptyObject2.transform);
_CpArr.Add(_EmptyObject3.transform);
_CpArr.Add(_EmptyObject4.transform);
_CpArr.Add(_EmptyObject5.transform);
for (int i = 0; i < _CpArr.Count; i++)
{
if (_CpArr[i].GetComponent<ControlPoint_ZH>() == null)
{
//弯曲端点 受力组件添加
_CpArr[i].gameObject.AddComponent<ControlPoint_ZH>();
}
}
}
//作用力变量
float _CurrentNumber = 90;
if (_CpArr.Count > 0)
{
//6个控制点的情况下
//其他 自己调节一下
for (int i = 0; i < _CpArr.Count; i++)
{
//作用力赋值
_CpArr[i].GetComponent<ControlPoint_ZH>()._BendForce = _CurrentNumber;
_CurrentNumber /= 2;
}
}
if (_ForceItem==null)
{
_ForceItem = GameObject.Find("ForceCube").transform;
}
//顶部坐标 获取
_TopPos = _CpArr[_CpArr.Count - 1].TransformPoint(new Vector3(0, 0, 0));
//相对P0坐标 也就是第一个控制点坐标
_TopPos = _CpArr[0].InverseTransformPoint(_TopPos);
//网格获取
_Mesh = GetComponent<MeshFilter>().mesh;
//网格顶点位置获取
_OriVertices = (Vector3[])_Mesh.vertices.Clone();
//转换成p0的相对坐标
for (int i = 0; i < _OriVertices.Length; i++)
{
//世界坐标 局部转世界
_OriVertices[i] = transform.TransformPoint(_OriVertices[i]);
//相对P0坐标 世界转局部
_OriVertices[i] = _CpArr[0].InverseTransformPoint(_OriVertices[i]);
}
}
/// <summary>
/// 获取所有子节点
/// </summary>
/// <param 父节点="_Parent"></param>
/// <returns></returns>
private List<Transform> GetAllChildren(Transform _Parent)
{
List<Transform> _ListTra = new List<Transform>();
foreach (Transform _Child in _Parent)
{
//添加直接子节点
_ListTra.Add(_Child);
//递归收集后代节点
List<Transform> _Descendants = GetAllChildren(_Child);
_ListTra.AddRange(_Descendants);
}
return _ListTra;
}
/********************************贝塞尔曲线Mesh计算相关*********************************/
// 对原来的顶点做贝塞尔曲线变换,得到弯曲变换后对应的点位置
/// <summary>
/// 控制点更新
/// 根据施加在曲线上的力,计算并更新控制点的旋转角度
/// </summary>
private void UpdateBezierBend()
{
//曲线弯曲方向
Vector3 _BendVector = new Vector3(0, 0, 0);
//弯曲布尔 true 无弯曲 false 弯曲
bool _IsVertical = true;
for (int i = 1; i < _CpArr.Count; i++)
{
//对于每个控制点,将其位置转换为第一个控制点 _CpArr[0] 的局部空间
Vector3 _Pos = _CpArr[i].TransformPoint(new Vector3(0, 0, 0));
_Pos = _CpArr[0].InverseTransformPoint(_Pos);
//判断当前控制点 x z 是否在非零
//如果非零 就证明曲线在一个非垂直方向上弯曲
if (IsEqualZero(_Pos.x) == false || IsEqualZero(_Pos.z) == false)
{
//临时弯曲变量存储
_BendVector.x = _Pos.x;
_BendVector.z = _Pos.z;
//弯曲布尔
_IsVertical = false;
break;
}
}
//原始网格顶点数组
Vector3[] _Temp = (Vector3[])_Mesh.vertices.Clone();
//遍历所有顶点
for (int i = 0; i < _OriVertices.Length; i++)
{
//获取顶点坐标,计算t值
Vector3 _OriPos = _OriVertices[i];
Vector3 _BendPos;
//没有弯曲
if (_IsVertical == true)
{
//当前顶点位置不变
_BendPos = _OriPos;
}
//发生弯曲
else
{
//将顶点的原始位置 _OriPos 的 y 分量除以顶部控制点 _TopPos 的 y 分量,计算参数 _CurvePosition
float _T = _OriPos.y / _TopPos.y;
//获取顶点在贝塞尔曲线上对应的坐标
Vector3 _BezierPos = CalculateBezier(_T);
//获取顶点在曲线上应有的法线偏移向量
Vector3 _NormalVector = GetBendNormalVector(_T, _OriPos, _BendVector);
//获取顶点在曲线上应有的垂直偏移向量
Vector3 _VerticalVector = new Vector3(_OriPos.x, 0, _OriPos.z) - Vector3.Project(new Vector3(_OriPos.x, 0, _OriPos.z), _BendVector);
//获取顶点最终弯曲位置
_BendPos = _BezierPos + _NormalVector + _VerticalVector;
}
//转换回 mesh 本地坐标系
_BendPos = _CpArr[0].TransformPoint(_BendPos);
_BendPos = transform.InverseTransformPoint(_BendPos);
_Temp[i] = _BendPos;
}
_Mesh.vertices = _Temp;
}
/// <summary>
/// Bezier 曲线公式
/// 计算贝塞尔曲线上给定参数值 _CurvePosition 对应的位置
/// </summary>
/// <param 曲线位置="_CurvePosition"></param>
/// <returns></returns>
private Vector3 CalculateBezier(float _CurvePosition)
{
//存储坐标
Vector3 _Ret = new Vector3(0, 0, 0);
//控制点数量
int _Number = _CpArr.Count - 1;
for (int i = 0; i <= _Number; i++)
{
//获取第 i 个控制点的世界坐标,通过将其位置从控制点的局部坐标系转换到世界坐标
Vector3 _Pi = _CpArr[i].TransformPoint(new Vector3(0, 0, 0));
//将转换后的世界坐标再次转换回第一个控制点的局部坐标系,用于与其他控制点进行计算
_Pi = _CpArr[0].InverseTransformPoint(_Pi);
//根据贝塞尔曲线的定义,计算贝塞尔基函数的乘积项,并与控制点的局部坐标相乘
//Cn_m(_Number, i) 是组合数函数 表示从 _Number 个控制点中选择 i 个的组合数
_Ret = _Ret + Mathf.Pow(1 - _CurvePosition, _Number - i) * Mathf.Pow(_CurvePosition, i) * Cn_m(_Number, i) * _Pi;
}
return _Ret;
}
/// <summary>
/// 曲线求导(切线向量)
/// </summary>
/// <param 曲线位置="_CurvePosition"></param>
/// <returns></returns>
private Vector3 CalculateBezierTangent(float _CurvePosition)
{
//存储计算得到的切线向量
Vector3 _Ret = new Vector3(0, 0, 0);
//控制点数量
int _Number = _CpArr.Count - 1;
for (int i = 0; i <= _Number; i++)
{
Vector3 _Pi = _CpArr[i].TransformPoint(new Vector3(0, 0, 0));
_Pi = _CpArr[0].InverseTransformPoint(_Pi);
//根据贝塞尔曲线的定义,计算贝塞尔基函数的导数项,并与控制点的局部坐标相乘
//每次循环迭代,都将计算得到的导数项加到 _Ret 向量上
_Ret = _Ret + (-1 * (_Number - i) * Mathf.Pow(1 - _CurvePosition, _Number - i - 1) * Mathf.Pow(_CurvePosition, i) * Cn_m(_Number, i) * _Pi + i * Mathf.Pow(1 - _CurvePosition, _Number - i) * Mathf.Pow(_CurvePosition, i - 1) * Cn_m(_Number, i) * _Pi);
}
//返回计算得到的切线向量 _Ret,表示贝塞尔曲线上给定参数值 _CurvePosition 对应的切线向量
return _Ret;
}
/// <summary>
/// 获取指定点上的法线向量偏移
/// 计算在贝塞尔曲线上给定参数值 _CurvePosition 对应位置的弯曲法向量
/// </summary>
/// <param 曲线位置="_CurvePosition"></param>
/// <param 原始位置="_OriPos"></param>
/// <param 弯曲方向="_BendVector"></param>
/// <returns></returns>
private Vector3 GetBendNormalVector(float _CurvePosition, Vector3 _OriPos, Vector3 _BendVector)
{
//切线斜率
Vector3 _TangentVector = CalculateBezierTangent(_CurvePosition);
//切线竖直时,顶点在在弯曲向量上的投影向量即为法线向量
if (IsEqualZero(_TangentVector.x) == true && IsEqualZero(_TangentVector.z) == true)
{
//直接返回 原始位置 _OriPos 投影到弯曲向量 _BendVector 上的向量,作为法线向量
return Vector3.Project(new Vector3(_OriPos.x, 0, _OriPos.z), _BendVector);
}
//存储计算得到的法线向量
Vector3 _NormalVector = new Vector3(0, 0, 0);
//计算切线向量与原始位置向量的点乘
float _DirectFlag = Vector3.Dot(_BendVector, _OriPos);
//判断法线向量朝向(法线向量有两个方向)
//大于零 顶点坐标与弯曲方向同向
if (_DirectFlag > 0)
{
//切线水平,法线向量竖直向下
if (IsEqualZero(_TangentVector.y) == true)
{
_NormalVector.y = -1;
}
else
{
//切线朝上,法线向量与切线水平同向
if (_TangentVector.y > 0)
{
_NormalVector.x = _TangentVector.x;
_NormalVector.z = _TangentVector.z;
}
//切线朝下,法线向量与切线水平反向
else
{
_NormalVector.x = -_TangentVector.x;
_NormalVector.z = -_TangentVector.z;
}
//使法线向量与切线向量水平且垂直于切线向量
_NormalVector.y = -(_TangentVector.x * _NormalVector.x + _TangentVector.z * _NormalVector.z) / _TangentVector.y;
}
}
//小于零 顶点坐标与弯曲方向反向
else
{
//切线水平,法线向量竖直向上
if (IsEqualZero(_TangentVector.y) == true)
{
_NormalVector.y = 1;
}
else
{
//切线朝上,法线向量与切线水平反向
if (_TangentVector.y > 0)
{
_NormalVector.x = -_TangentVector.x;
_NormalVector.z = -_TangentVector.z;
}
//切线朝下,法线向量与切线水平同向
else
{
_NormalVector.x = _TangentVector.x;
_NormalVector.z = _TangentVector.z;
}
//使得法线向量与切线向量水平且垂直于切线向量
_NormalVector.y = -(_TangentVector.x * _NormalVector.x + _TangentVector.z * _NormalVector.z) / _TangentVector.y;
}
}
//计算法线向量的模
//法线向量的模应为到投影到弯曲面后,到中心点的距离
float _Magnitude = Vector3.Project(new Vector3(_OriPos.x, 0, _OriPos.z), _BendVector).magnitude;
//将法线向量按照该模进行缩放,使其长度与距离一致
_NormalVector = _NormalVector.normalized * _Magnitude;
//返回法线向量
return _NormalVector;
}
/// <summary>
/// 浮点判断是否为零
/// </summary>
/// <param 值="_Value"></param>
/// <returns></returns>
private bool IsEqualZero(float _Value)
{
return Mathf.Abs(_Value) < 1e-5;
}
/// <summary>
/// 组合数函数
/// 表示从 _Number 个控制点中选择 i 个的组合数
/// </summary>
/// <param 控制点="_Number"></param>
/// <param 组合数="m"></param>
/// <returns></returns>
private int Cn_m(int n, int m)
{
int _Ret = 1;
for (int i = 0; i < m; i++)
{
_Ret = _Ret * (n - i) / (i + 1);
}
return _Ret;
}
/************************************根据受力情况计算控制点坐标(旋转)*****************************/
/// <summary>
/// 更新控制点的旋转角度
/// 根据受力计算各个控制点的旋转角度
/// </summary>
private void UpdateControlPoint()
{
//受力强度
float _HandleForce = _Force;
//根据受力计算各个控制点旋转角度
for (int i = 1; i <= _CpArr.Count - 2; i++)
{
//计算最大弯曲方向
Vector3 _ForcePos = _ForceItem.transform.TransformPoint(new Vector3(0, 0, 0));
_ForcePos = _CpArr[i - 1].InverseTransformPoint(_ForcePos);
//计算从控制点到受力位置的向量
Vector3 _ToVector = _ForcePos - _CpArr[i].localPosition;
//得到该控制点的旋转角度
Quaternion _MaxRotation = Quaternion.FromToRotation(Vector3.up, _ToVector);
//获取控制点组件
ControlPoint_ZH _Cp = _CpArr[i].gameObject.GetComponent<ControlPoint_ZH>();
//计算弯曲比例
float _RotateRate = Mathf.Clamp(_HandleForce / _Cp._BendForce, 0f, 1.0f);
//设置旋转角度
_CpArr[i].localRotation = Quaternion.Lerp(Quaternion.Euler(0, 0, 0), _MaxRotation, _RotateRate);
}
}
}
自定义网格创建
在Unity中,网格是由一系列顶点、三角形面和纹理坐标组成的。
通过创建网格,我们可以实现各种形状和模型的生成。
在示例代码中,我们可以看到通过使用Unity的Mesh和MeshFilter组件。
以及C#脚本中的数据结构和方法,实现了一个具有多个相位的简单网格的动态生成。
该网格可以用于游戏场景、建筑模型等各种应用。
使用Unity的 Mesh 和 MeshFilter 组件:这些组件允许我们创建和管理网格。
Mesh 用于存储网格的几何数据,而 MeshFilter 用于将Mesh附加到游戏对象上。
GridCreation_ZH 完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 网格创建
/// 动态生成一个具有多个相位的简单网格,可以根据需求调整网格的宽度、高度、相位数量和末端宽度
/// 以获得不同形状和细分程度的网格
/// </summary>
public class GridCreation_ZH : MonoBehaviour
{
//单例
public static GridCreation_ZH _Instance;
[Header("网格宽度")]
public float _MeshWidth = 1f;
[Header("网格高度")]
public float _MeshHeight = 10f;
[Header("网格分段数")]
public int _PhaseCount = 20;
[Header("网格末端宽度")]
public float _EndWidth = 0.1f;
void Awake()
{
_Instance = this;
//计算每个相位的高度
float _PhaseHeight = _MeshHeight / (_PhaseCount - 1);
//计算每个相位的宽度
float _DecreaseWidth = (_MeshWidth - _EndWidth) / (_PhaseCount - 1);
//顶点数组
//长度为 _PhaseCount * 6,每个相位有 6 个顶点
Vector3[] _Vertices = new Vector3[_PhaseCount * 6];
float _BottomY = -_MeshHeight * 0.5f;
for (int i = 0; i < _PhaseCount; i++)
{
//根据当前相位的索引计算每个顶点的位置
//并将其存储在 _Vertices 数组中
float _CurWidth = _MeshWidth - _DecreaseWidth * i;
_Vertices[i * 6 + 0] = new Vector3(_BottomY + i * _PhaseHeight, (_CurWidth / 2), 0);
_Vertices[i * 6 + 1] = new Vector3(_BottomY + i * _PhaseHeight, (_CurWidth / 4), -Mathf.Sqrt(3) * _CurWidth / 4);
_Vertices[i * 6 + 2] = new Vector3(_BottomY + i * _PhaseHeight, -(_CurWidth / 4), -Mathf.Sqrt(3) * _CurWidth / 4);
_Vertices[i * 6 + 3] = new Vector3(_BottomY + i * _PhaseHeight, -(_CurWidth / 2), 0);
_Vertices[i * 6 + 4] = new Vector3(_BottomY + i * _PhaseHeight, -(_CurWidth / 4), Mathf.Sqrt(3) * _CurWidth / 4);
_Vertices[i * 6 + 5] = new Vector3(_BottomY + i * _PhaseHeight, (_CurWidth / 4), Mathf.Sqrt(3) * _CurWidth / 4);
}
//法线数组
//长度也为 _PhaseCount * 6,与顶点数组相对应
Vector3[] _Normals = new Vector3[_PhaseCount * 6];
for (int i = 0; i < _PhaseCount; i++)
{
//根据当前相位的索引计算每个顶点的法线方向,并将其存储在 _Normals 数组中
float _CurWidth = _MeshWidth - _DecreaseWidth * i;
_Normals[i * 6 + 0] = new Vector3(0, (_CurWidth / 2), 0).normalized;
_Normals[i * 6 + 1] = new Vector3(0, (_CurWidth / 4), -Mathf.Sqrt(3) * _CurWidth / 4).normalized;
_Normals[i * 6 + 2] = new Vector3(0, -(_CurWidth / 4), -Mathf.Sqrt(3) * _CurWidth / 4).normalized;
_Normals[i * 6 + 3] = new Vector3(0, -(_CurWidth / 2), 0).normalized;
_Normals[i * 6 + 4] = new Vector3(0, -(_CurWidth / 4), Mathf.Sqrt(3) * _CurWidth / 4).normalized;
_Normals[i * 6 + 5] = new Vector3(0, (_CurWidth / 4), Mathf.Sqrt(3) * _CurWidth / 4).normalized;
}
//三角形索引数组
//长度为 (_PhaseCount - 1) * 6 * 6,每个相位之间的相邻顶点构成一个三角形
int[] _Indices = new int[(_PhaseCount - 1) * 6 * 6];
for (int i = 1; i < _PhaseCount; i++)
{
for (int j = 0; j < 6; j++)
{
//嵌套循环遍历,根据相邻顶点的索引,构建三角形的索引,并将其存储在 _Indices 数组中
int nextIndex = (j + 1) % 6;
_Indices[(i - 1) * 6 * 6 + j * 6 + 0] = (i - 1) * 6 + j;
_Indices[(i - 1) * 6 * 6 + j * 6 + 1] = i * 6 + nextIndex;
_Indices[(i - 1) * 6 * 6 + j * 6 + 2] = (i - 1) * 6 + nextIndex;
_Indices[(i - 1) * 6 * 6 + j * 6 + 3] = (i - 1) * 6 + j;
_Indices[(i - 1) * 6 * 6 + j * 6 + 4] = i * 6 + j;
_Indices[(i - 1) * 6 * 6 + j * 6 + 5] = i * 6 + nextIndex;
}
}
//将之前计算得到的顶点、法线和三角形索引分别赋值给网格的对应属性,创建了一个简单的网格
Mesh _Mesh = GetComponent<MeshFilter>().mesh;
_Mesh.vertices = _Vertices;
_Mesh.normals = _Normals;
_Mesh.triangles = _Indices;
transform.position = Vector3.up * 5;
transform.eulerAngles = new Vector3(0, 0, 90);
}
}
GridCreation_ZH 搭载
记得在空物体上添加:Mesh Renderer、Mesh Filter 组件。
Hierarchy 窗口:
自定义网格创建 运行效果
网格创建和曲线变形 运行情况
Hierarchy 窗口:
运行效果:
除了以上示例代码,还有其他相关的技术和工具可以扩展和优化网格创建和曲线变形效果。
例如,使用Shader进行高级的顶点变形和着色效果、使用插件或库来简化曲线绘制和变形操作等。
通过深入学习和实践这些技术,您可以在Unity中创建出更加生动和富有创意的场景和模型。
掌握网格创建和曲线变形的原理和方法,将为您的游戏开发和视觉效果设计带来更多的可能性和灵活性。
希望这些信息能够进一步满足您对Unity中 网格创建和曲线变形的需求。
如果您有任何特定的问题或需要更深入的讨论,请随时提出。文章来源:https://www.toymoban.com/news/detail-842983.html
路漫漫其修远兮,与君共勉。文章来源地址https://www.toymoban.com/news/detail-842983.html
到了这里,关于Unity中的网格创建和曲线变形的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!