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高度来计算t
    private 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 分量,计算参数 _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中 网格创建和曲线变形的需求。
如果您有任何特定的问题或需要更深入的讨论,请随时提出。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JumpServer部署使用

1. 简介 JumpServer 是全球首款开源的堡垒机&#xff0c;使用 GNU GPL v3.0 开源协议&#xff0c;是符合 4A 规范的运维安全审计系统&#xff0c;使用 Python 开发&#xff0c;遵循 Web 2.0 规范&#xff0c;配备了业界领先的 Web Terminal 方案&#xff0c;交互界面美观、用户…

常见的实时操作系统(RTOS)(嵌入式和物联网操作系统)介绍

在嵌入式系统和物联网&#xff08;IoT&#xff09;设备中&#xff0c;实时操作系统&#xff08;RTOS&#xff09;是至关重要的&#xff0c;因为它们负责管理有限的硬件资源&#xff0c;并提供确保任务在特定时间内完成的机制。开源实时操作系统&#xff08;RTOS&#xff09;允许…

【Python】清理conda缓存的常用命令

最近发现磁盘空间不足&#xff0c;很大一部分都被anaconda占据了&#xff0c;下面是一些清除conda缓存的命令 清理所有环境的Anaconda包缓存 删除所有未使用的包以及缓存的索引和临时文件 conda clean --all清理某一特定环境的Anaconda包缓存 conda clean --all -n 环境名清…

离线安装docker、docker-compose、Mysql镜像

离线安装docker docker-compose mysql镜像 一、下载docker docker-compose mysql 镜像文件 1、首先下载docker镜像 博主所用文件版本号&#xff1a; docker-23.0.6.tgz 下载docker 地址 &#xff1a;https://blog.csdn.net/xiaohanshasha/article/details/135489623?spm1001…

Vue前端开发记录(一)

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

移远通信亮相AWE 2024,以科技力量推动智能家居产业加速发展

科技的飞速发展&#xff0c;为我们的生活带来了诸多便利&#xff0c;从传统的家电产品到智能化的家居设备&#xff0c;我们的居家生活正朝着更智能、更便捷的方向变革。 3月14日&#xff0c;中国家电及消费电子博览会&#xff08;Appliance&electronics World Expo&#xf…

腾讯云对象存储的在Java使用步骤介绍

腾讯云对象存储的在Java使用步骤介绍 创建一个腾讯云的账户 这个自己去创建&#xff0c;然后开通对象存储服务&#xff0c;如下 有了账号就要创建一个对象存储的桶&#xff0c;也就是存储对象的一个容器 如果你想只有自己放存放的就设置 私有读写 &#xff0c;如果想用网络直…

fs模块 练习题

编写程序&#xff1a; 在文件夹data里有文件a.txt和文件b.txt。将文件a.txt和文件b.txt的内容读取后写入文件c.txt中。 一、同步读取 const fs require(fs); let datas1fs.readFileSync(./data/a.txt); let datas2fs.readFileSync(./data/b.txt); let datas3datas1datas2;//对…

智慧城市:提升城市治理能力的关键

目录 一、智慧城市的概念及特点 二、智慧城市在提升城市治理能力中的应用实践 1、智慧交通&#xff1a;提高交通治理效率 2、智慧政务&#xff1a;提升政府服务水平 3、智慧环保&#xff1a;加强环境监测与治理 4、智慧安防&#xff1a;提高城市安全水平 三、智慧城市在…

多人聊天室 (epoll - Linux网络编程)

文章目录 零、效果展示一、服务器代码二、客户端代码三、知识点1.bind() 四、改进方向五、跟练视频 零、效果展示 一个服务器作为中转站&#xff0c;多个客户端之间可以相互通信。至少需要启动两个客户端。 三个客户端互相通信 一、服务器代码 chatServer.cpp #include <…

微信小程序--开启下拉刷新页面

1、下拉刷新获取数据enablePullDownRefresh 开启下拉刷新&#xff1a; enablePullDownRefreshbooleanfalse是否开启当前页面下拉刷新 案例&#xff1a; 下拉刷新&#xff0c;获取新的列表数据,其实就是进行一次新的网络请求&#xff1a; 第一步&#xff1a;在.json文件中开…

FPGA高端项目:FPGA基于GS2971+GS2972架构的SDI视频收发+OSD动态字符叠加,提供1套工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐本博已有的 SDI 编解码方案本方案的SDI接收发送本方案的SDI接收图像缩放应用本方案的SDI接收纯verilog图像缩放纯verilog多路视频拼接应用本方案的SDI接收HLS图像缩放HLS多路视频拼接应用本方案的SDI接收HLS多路视频融合叠加应用本方案的S…

sqllab第二十二关通关笔记

知识点&#xff1a; cookie注入报错注入 直接抓取对应的数据包&#xff0c;发现还是一个cookie注入 参数值被base64加密了 测试这里使用什么手段读取输入 构造payload:uname1 base64加密&#xff1a;MSc 出现了hacker的页面&#xff0c;说明信息错误但是单引号没起作用 使…

《ARM汇编与逆向工程》读书心得与实战体验

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 &#x1f4d8; 一、引言 &#x1f4dd; 二、…

Livox激光雷达 mid360 跑 fastlio2 - 流程记录

mid360 跑 fastlio2 一、配置 mid360 环境1.1、主机配置静态IP为192.168.1.501.2、Livox-SDK21.3、Livox_ros_driver2二、Fast-lio22.1、下载源码2.2、修改代码2.3、编译、运行 一、配置 mid360 环境 1.1、主机配置静态IP为192.168.1.50 1.2、Livox-SDK2 安装工具 sudo apt…

【Unity+Vuforia】AR 发布安卓的设置

Player Settings > Resolution and Presentation > Default Orientation portrait Player Settings > Other Settings > Auto Graphics API 取消勾选 Player Settings > Other Settings > Graphics APIs 选择OpenGLES3删除其他的 Player Settings…

Python QT 之PySide6简单入门

目录 1.开发环境配置 1.1 下载PySide6 2.2 配置pycharm相关快捷方式 PySide6_Designer - QT Designer 设计UI PySide6_UIC - 将QT Designer生成的UI文件转换为python文件 PySide6_RCC - 将RCC文件转换为python文件 2.第一个开发实例 2.1 QT desiger设计界面 2.2 将ui文…

知名比特币质押协议项目Babylon联合创始人David将出席参加Hack.Summit()2024区块链开发者大会

Babylon项目已确认将派遣其项目代表出席2024年在香港数码港举办的Hack.Summit()2024区块链开发者大会。作为比特币生态的领军项目&#xff0c;Babylon积极参与全球区块链领域的交流与合作&#xff0c;此次出席大会将为其提供一个展示项目进展、交流技术与创新思路的重要平台。B…

信雅纳网络测试的二次开发集成:XOA(Xena Open-Source Automation)开源自动化测试

目录 XOA是什么 XOA CLI XOA Python API ​XOA Python Test Suite/测试套件 XOA Converter Source Code XOA是什么 XOA&#xff08;Xena Open-Source Automation&#xff09;是一个开源的测试自动化框架&#xff0c;追求“高效、易用、灵活”的跨操作系统的开发框架。能…

开启Safari手势支持

在使用Safari 的时候&#xff0c;大家有没有觉得不支持手势使用起来不是很方便&#xff0c; 触摸板只支持少量简单的手势&#xff0c;如缩放&#xff0c;滚动等。如果使用鼠标的用户&#xff0c;则完全无法使用手势。经过折腾研究&#xff0c;使用CirMenu应用可以完美解决这个要…