Unity中的网格创建和曲线变形

Unity中的网格创建和曲线变形

  • 3D贝塞尔曲线变形
    • 贝塞尔曲线基础
      • 线性公式
      • 二次方公式
      • 三次方公式
    • Unity 实现3D贝塞尔曲线变形
      • 准备工作
      • 脚本概述
        • 变量定义
      • 变量解析
      • 函数解析 获取所有子节点
        • GetAllChildren 获取所有子节点
        • UpdateBezierBend 控制点更新
        • CalculateBezier Bezier 曲线公式
        • GetBendNormalVector 获取指定点上的法线向量偏移
        • UpdateControlPoint 更新控制点的旋转角度
        • CalculateBezierTangent 曲线求导(切线向量)
      • BesselCurveDeformation_ZH 完整代码
    • 自定义网格创建
      • GridCreation_ZH 完整代码
      • GridCreation_ZH 搭载
      • 自定义网格创建 运行效果
    • 网格创建和曲线变形 运行情况

在本篇博客中,我们将探讨如何使用Unity实现3D贝塞尔曲线变形效果。

3D贝塞尔曲线变形

贝塞尔曲线是一种常用的数学曲线,通过控制点和曲线影响力物体我们可以实现对网格的弯曲和变形。
在示例代码中,我们可以看到通过设置控制点数组、曲线影响力物体和控制物体曲线施加力等变量。
实现了对网格进行贝塞尔曲线变形的效果。这种技术在游戏中常用于创建动态的形变效果。
如弯曲的绳索、变形的角色模型等。

贝塞尔曲线基础

贝塞尔曲线是一种由控制点定义的数学曲线。
在3D空间中,我们通常使用3次贝塞尔曲线,它由四个控制点(起始点、两个中间点和结束点)组成。
贝塞尔曲线的形状受控制点的位置和权重影响。
控制点的位置决定了曲线经过的路径,而权重控制了曲线在控制点之间的弯曲程度。

请添加图片描述

线性公式

给定点P0P1,线性贝兹曲线只是一条两点之间的直线。

请添加图片描述

请添加图片描述

二次方公式

二次方贝兹曲线的路径由给定点P0P1P2的函数B(t)追踪:
TrueType字型就运用了以贝兹样条组成的二次贝兹曲线。

请添加图片描述

请添加图片描述

三次方公式

P0P1P2P3四个点在平面或在三维空间中定义了三次方贝兹曲线。
曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1P2。
这两个点只是在那里提供方向资讯。P0P1之间的间距,决定了曲线在转而趋进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高度来计算tprivate Vector3 _TopPos;

变量解析

_CpArr: 存储贝塞尔曲线控制点数组的列表。
_ForceItem: 曲线影响力物体的Transform组件。
_Force: 控制物体曲线施加力的大小,范围在0100之间。
_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 分量,计算参数 _CurvePositionfloat _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高度来计算tprivate Vector3 _TopPos;void Start(){Initialize();}void Update(){//根据受力,修改控制点UpdateControlPoint();//更新meshUpdateBezierBend();}/// <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 分量,计算参数 _CurvePositionfloat _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中 网格创建和曲线变形的需求。
如果您有任何特定的问题或需要更深入的讨论,请随时提出。

路漫漫其修远兮,与君共勉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/747793.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【算法杂货铺】二分算法

目录 &#x1f308;前言&#x1f308; &#x1f4c1; 朴素二分查找 &#x1f4c2; 朴素二分模板 &#x1f4c1; 查找区间端点处 细节&#xff08;重要&#xff09; &#x1f4c2; 区间左端点处模板 &#x1f4c2; 区间右端点处模板 &#x1f4c1; 习题 1. 35. 搜索插入位…

phpcms头像上传漏洞引发的故事

目录 关键代码 第一次防御 第一次绕过 第二次防御 第二次绕过 第三次防御 第三次绕过 如何构造一个出错的压缩包 第四次防御 第四次绕过 本篇文章是参考某位大佬与开发人员对于文件包含漏洞的较量记录下的故事&#xff0c;因为要学习文件包含漏洞&#xff0c;就将大佬…

什么是 HTTPS?它是如何解决安全性问题的?

什么是 HTTPS&#xff1f; HTTPS&#xff08;HyperText Transfer Protocol Secure&#xff09;是一种安全的通信协议&#xff0c;用于在计算机网络上安全地传输超文本&#xff08;如网页、图像、视频等&#xff09;和其他数据。它是 HTTP 协议的安全版本&#xff0c;通过使用加…

Java开发从入门到精通(九):Java的面向对象OOP:成员变量、成员方法、类变量、类方法、代码块、单例设计模式

Java大数据开发和安全开发 &#xff08;一)Java的变量和方法1.1 成员变量1.2 成员方法1.3 static关键字1.3.1 static修饰成员变量1.3.1 static修饰成员变量的应用场景1.3.1 static修饰成员方法1.3.1 static修饰成员方法的应用场景1.3.1 static的注意事项1.3.1 static的应用知识…

微服务技术栈SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式(五):分布式搜索 ES-中

文章目录 一、DSL查询文档1.1 简单查询1.2 复合查询 二、搜索结果处理三、RestClient演示 查询与结果分析四、案例4.1 问题解析4.2 代码4.2.1 实体bean4.2.2 控制层4.2.3 业务service4.2.4 启动类 一、DSL查询文档 1.1 简单查询 # 1. DSL查询 # 1.1 查询所有GET /hotel/_searc…

JavaScript 进阶(一)

一、作用域 作用域&#xff08;scope&#xff09;规定了变量能够被访问的“范围”&#xff0c;离开了这个“范围”变量便不能被访问。 作用域分为&#xff1a; 局部作用域 、全局作用域。 1.1局部作用域 局部作用域分为函数作用域和块作用域。 1. 函数作用域&#xff1a; 在函数…

分布式系统常见负载均衡实现模式

分布式系统常见负载均衡实现模式 1. 4层负载均衡1.1. 负载均衡的常见需求1.2. 负载均衡的实现模式1.2.1 DR模式1.2.2 TUN模式1.2.3 NAT模式1.2.4 FULLNAT模式1.2.5 4种模式的差异 1.3. 负载均衡的均衡算法1.3.1 静态负载均衡1.3.2 轮询法1.3.3 加权循环法1.3.4 IP 哈希法1.3.5 …

Vue前端开发记录(一)

本篇文章中的图片均为深色背景&#xff0c;请于深色模式下观看 说明&#xff1a;本篇文章的内容为vue前端的开发记录&#xff0c;作者在这方面的底蕴有限&#xff0c;所以仅作为参考 文章目录 一、安装配置nodejs,vue二、vue项目目录结构三、前期注意事项0、组件1、数不清的报…

首个ChatGPT机器人- Figure 01;李开复旗下零一万物推出Yi系列AI大模型API

&#x1f989; AI新闻 &#x1f680; 首个ChatGPT机器人- Figure 01 摘要&#xff1a;Figure 01是一个由初创公司Figure联合OpenAI开发的人形机器人。它展示了与人类和环境互动的能力&#xff0c;可以说话、看东西&#xff0c;并且可以执行各种任务&#xff0c;如递食物、捡垃…

QT中dumpcpp以及dumpdoc使用

qt中调用COM的方式方法有四种&#xff0c;参考解释在 Qt 中使用 ActiveX 控件和 COM (runebook.dev) 介绍dumpcpp的使用方法Qt - dumpcpp 工具 (ActiveQt) (runebook.dev)&#xff1a; 在安装好了的qt电脑上&#xff0c;通过powershell窗口来实现&#xff0c;powershell比cmd要…

算法的基本概念和复杂度

目录 一. 算法的基本概念1.1 什么是算法1.2 算法的五个特性1.3 怎么才算好的算法 二. 算法的时间复杂度三. 算法的空间复杂度 \quad 一. 算法的基本概念 \quad \quad 1.1 什么是算法 算法可以用自然语言来描述, 也可以用伪代码和代码来描述 \quad 1.2 算法的五个特性 有穷性, 一…

ASP.NET-Server.HtmlEncode

目录 背景: 1.转义特殊字符&#xff1a; 2.防止跨站脚本攻击&#xff08;XSS&#xff09;&#xff1a; 3.确保输出安全性&#xff1a; 4.保留原始文本形式&#xff1a; 5.与用户输入交互安全&#xff1a; 实例说明: 不用Server.HtmlEncode 效果展示: 用Server.HtmlEnc…

SpringMVC重点记录

目录 1.学习重点2.回顾MVC3.回顾servlet4.初始SpringMVC4.1.为什么要学SpringMVC?4.2.SpringMVC的中重点DispatcherServlet4.3.SpringMVC项目的搭建4.4.MVC框架要做哪些事情?4.5.可能会遇到的问题 5.SpringMVC的执行原理6.使用注解开发SpringMVC7.Controller控制总结8.RestF…

VSCode ARM CortexM 开发

VSCode ARM CortexM 开发: http://coffeelatte.vip.cpolar.top/post/software/applications/vscode/vscode_arm_cortexm_开发/ 文章目录 VSCode ARM CortexM 开发: <http://coffeelatte.vip.cpolar.top/post/software/applications/vscode/vscode_arm_cortexm_%E5%BC%80%E5%…

鸿蒙实战开发:【分布式软总线组件】

简介 现实中多设备间通信方式多种多样(WIFI、蓝牙等)&#xff0c;不同的通信方式使用差异大&#xff0c;导致通信问题多&#xff1b;同时还面临设备间通信链路的融合共享和冲突无法处理等挑战。分布式软总线实现近场设备间统一的分布式通信管理能力&#xff0c;提供不区分链路…

音频占用磁盘空间太多 需要把mp3音频转aac音频缩小占用空间 应该怎么操作?

一&#xff1a;什么是aac格式&#xff1f; aac是一种音频压缩格式&#xff0c;它是MPEG-2标准下的一种音频压缩方式&#xff0c;也可以作为HE-AAC&#xff0c;AAC或AAC-LC格式使用&#xff0c;是音频压缩领域中的一种重要格式。与MP3的比较&#xff0c;aac在保证音质的同时可以…

K8S CNI

OCI概念 OCI&#xff0c;Open Container Initiative&#xff0c;开放容器标准&#xff0c;是一个轻量级&#xff0c;开放的治理结构&#xff08;项目&#xff09;&#xff0c;在 Linux 基金会的支持下成立&#xff0c;致力于围绕容器格式和运行时创建开放的行业标准。 OCI 项目…

stm32-定时器输入捕获

目录 一、输入捕获简介 二、输入捕获框图 1.定时器总框图 2.输入捕获框图 3.主从触发模式 三、固件库实现 1.定时器测量PWM频率 2.PWMI模式 一、输入捕获简介 二、输入捕获框图 1.定时器总框图 上图可知&#xff0c;四个输入捕获和输出比较共用4个CCR寄存器&#x…

Android SystemServer进程解析

SystemServer进程在android系统中占了举足轻重的地位&#xff0c;系统的所有服务和SystemUI都是由它启动。 一、SystemServer进程主函数流程 1、主函数三部曲 //frameworks/base/services/java/com/android/server/SystemServer.java /** * The main entry point from zy…

人工智能程序使用的编程语言

用C语言可以写人工智能程序吗&#xff1f; 可以用C语言编写具有人工智能功能的程序&#xff0c;但是较为复杂。C语言是一种通用的编程语言&#xff0c;它在执行速度和资源控制方面表现出色&#xff0c;这使得它适合于需要高性能处理的人工智能应用&#xff0c;如游戏AI&#xf…