转载自 【Unity】弹性鱼竿简单实现-通过贝塞尔曲线修改Mesh
实现思路
弹性鱼竿,即可以根据受力状态自由弯曲的鱼竿,如何实现“弯曲”是关键。说到弯曲,自然而然想到曲线,从曲线的角度出发,那么关键就是如何生成曲线,以及如何根据曲线修改物体形状,从而达到弯曲的效果。
生成曲线的话,可以直接想到用贝塞尔曲线,由n个控制点绘制出n阶贝塞尔曲线,通过修改控制点的坐标来控制曲线变化。
然后我们可以考虑修改模型的Mesh顶点坐标来实现弯曲效果。
鱼竿本身
贝塞尔曲线公式
贝塞尔曲线就不详细介绍了,具体可以参考百度百科。贝塞尔曲线简单来说就是通过n个控制点来生成曲线,而修改控制点位置可以修改曲线的形状。这里有个二阶贝塞尔曲线在线模拟工具,可以体验一下。
首先我们要实现贝塞尔曲线的计算公式,可知其n阶曲线的公式为:
实现代码如下:
//贝塞尔曲线公式
private Vector3 CalculateBezier(float t)
{Vector3 ret = new Vector3(0, 0, 0);int n = 阶数;for(int i = 0; i <= n; i++){Vector3 pi = 第i个控制点的坐标;ret = ret + Mathf.Pow(1 - t, n - i) * Mathf.Pow(t, i) * Cn_m(n, i) * pi;}return ret;
}
//组合数方程
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;
}
其中控制点可以使用n个空节点来代替,控制点的坐标即为空节点的坐标。至于t值,可以看作顶点到鱼竿底部的距离与整个鱼竿长度的比值,0<= t <=1。这样设计的话,我们第一个控制点P0应该在鱼竿底部位置,而最后一个控制点Pn应该在鱼竿顶部位置。
模型应用曲线
当然,按照上面公式计算出的只是一条曲线,而我们的目的是模型能按照这个曲线进行弯曲,如示意图:
可以看出,我们计算出来的曲线其实是图中的中心线,而mesh顶点应该位于中心线的两侧,所以顶点弯曲后的坐标是应该要由贝塞尔曲线计算的坐标经过一定变换得来。
经过观察可以发现,弯曲后顶点的坐标P’应由计算出的曲线上的坐标P进行两次偏移得出:
在该点法线方向上进行偏移a 、在垂直于弯曲面的方向上进行偏移b ⃗
代码如下
// 对原来的顶点做贝塞尔曲线变换,得到弯曲变换后对应的点位置
private void UpdateBezierBend()
{ oriVertices = 模型未弯曲时的顶点数组;topPos = 最后一个控制点的坐标,用来计算模型长度;bendVector = 弯曲方向;for(int i = 0; i < oriVertices.Length; i++){//获取顶点坐标,计算t值Vector3 oriPos = oriVertices[i];float t = oriPos.y / topPos.y;//获取顶点在贝塞尔曲线上对应的坐标Vector3 p = CalculateBezier(t); //获取顶点在曲线上应有的法线偏移向量Vector3 vectorA = GetBendNormalVector(t, oriPos, bendVector); //获取顶点在曲线上应有的垂直偏移向量Vector3 vectorB = new Vector3(oriPos.x, 0, oriPos.z) - Vector3.Project(new Vector3(oriPos.x, 0, oriPos.z), bendVector); //获取顶点最终弯曲位置vector3 p' = p + vectorA + vectorB;}todo-修改顶点坐标;
}
// 获取指定点上的法向量偏移
private Vector3 GetBendNormalVector(float t, Vector3 oriPos, Vector3 bendVector)
{Vector3 tangentVector = CalculateBezierTangent(t);//切线斜率Vector3 normalVector = 由法线和切线互相垂直计算出法线方向;//法线向量的模应为到投影到弯曲面后,到中心点的距离float magnitude = Vector3.Project(new Vector3(oriPos.x, 0, oriPos.z), bendVector).magnitude;normalVector = normalVector.normalized * magnitude;return normalVector;
}
//对曲线公式求导得出切线向量
private Vector3 CalculateBezierTangent(float t)
{Vector3 ret = new Vector3(0, 0, 0);int n = 阶数;for(int i = 0; i <= n; i++){Vector3 pi = 第i个控制点的坐标;ret = ret + (-1 * (n - i) * Mathf.Pow(1 - t, n - i - 1) * Mathf.Pow(t, i) * Cn_m(n, i) * pi + i * Mathf.Pow(1 - t, n - i) * Mathf.Pow(t, i - 1) * Cn_m(n, i) * pi);}return ret;
}
这样我们就实现了通过控制点生成曲线,通过曲线弯曲物体的方法。如图:
简单构造受力模型
接下来我们简单构造一个受力模型,通过物体施加拉力,拉力使控制点发生变化,从而使物体弯曲。我们简单设定一个Cube为施加拉力F的物体,然后为每个控制点设定一个完全弯曲所需要的力Fc,然后设定控制点朝拉力方向弯曲的角度为:
a = Mathf.Clamp(F/Fc, 0, 1.0) * 拉力与控制点的夹角;
为了模拟比较真实的弯曲效果,Fc可以看成每节竿子的弹力大小,越靠近底部的控制点Fc就越大,越难弯曲,反之,越靠近竿顶的控制点Fc越小,也就越容易弯曲。
代码如下:
private void UpdateControlPoint()
{
float F = Cube.force;
//根据受力计算各个控制点旋转角度
n = 控制点数量;
for(int i = 1; i < n - 1; i++)//第一个和最后一个点不计算弯曲
{
//计算最大弯曲方向
Vector3 toVector = 施力物体相对控制点pi的方向;
Quaternion maxRotation = Quaternion.FromToRotation(Vector3.up, toVector);
//计算弯曲比例
float rotateRate = Mathf.Clamp(F / Fc, 0f, 1.0f);
//设置旋转角度
pi.localRotation = Quaternion.Lerp(Quaternion.Euler(0, 0, 0), maxRotation, rotateRate);
}
}
效果如图:
全部代码
Bezier3D
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bezier3D : MonoBehaviour
{
//控制点
public Transform[] cpArr;
//施力物体
public ForceItem forceItem;
//原顶点位置
private Vector3[] oriVertices;
private Mesh m_Mesh;
private Vector3 topPos;//最后一个控制点的位置。用来计算mesh高度来计算t
void Start()
{
topPos = cpArr[cpArr.Length - 1].TransformPoint(new Vector3(0, 0, 0));//顶部坐标
topPos = cpArr[0].InverseTransformPoint(topPos);//相对p0坐标
m_Mesh = GetComponent<MeshFilter>().mesh;
oriVertices = (Vector3 [])m_Mesh.vertices.Clone();
//转换成p0的相对坐标
for(int i = 0; i < oriVertices.Length; i++)
{
oriVertices[i] = transform.TransformPoint(oriVertices[i]);//世界坐标
oriVertices[i] = cpArr[0].InverseTransformPoint(oriVertices[i]);//相对p0坐标
}
}
void Update()
{
//根据受力,修改控制点
UpdateControlPoint();
//更新mesh
UpdateBezierBend();
}
/********************************贝塞尔曲线Mesh计算相关*********************************/
// 对原来的顶点做贝塞尔曲线变换,得到弯曲变换后对应的点位置
private void UpdateBezierBend()
{
//判断曲线弯曲方向
Vector3 bendVector = new Vector3(0, 0, 0);
bool isVertical = true;
for(int i = 1; i < cpArr.Length; i++)
{
Vector3 pos = cpArr[i].TransformPoint(new Vector3(0, 0, 0));
pos = cpArr[0].InverseTransformPoint(pos);
if(IsEqualZero(pos.x) == false || IsEqualZero(pos.z) == false)
{
bendVector.x = pos.x;
bendVector.z = pos.z;
isVertical = false;
break;
}
}
Vector3[] temp = (Vector3 [])m_Mesh.vertices.Clone();
for(int i = 0; i < oriVertices.Length; i++)
{
//获取顶点坐标,计算t值
Vector3 oriPos = oriVertices[i];
Vector3 bendPos;
if(isVertical == true)
{
bendPos = oriPos;
}
else
{
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;
}
m_Mesh.vertices = temp;
}
//bezier曲线公式
private Vector3 CalculateBezier(float t)
{
Vector3 ret = new Vector3(0, 0, 0);
int n = cpArr.Length - 1;
for(int i = 0; i <= n; i++)
{
Vector3 pi = cpArr[i].TransformPoint(new Vector3(0, 0, 0));
pi = cpArr[0].InverseTransformPoint(pi);
ret = ret + Mathf.Pow(1 - t, n - i) * Mathf.Pow(t, i) * Cn_m(n, i) * pi;
}
return ret;
}
//曲线求导(切线向量)
private Vector3 CalculateBezierTangent(float t)
{
Vector3 ret = new Vector3(0, 0, 0);
int n = cpArr.Length - 1;
for(int i = 0; i <= n; i++)
{
Vector3 pi = cpArr[i].TransformPoint(new Vector3(0, 0, 0));
pi = cpArr[0].InverseTransformPoint(pi);
ret = ret + (-1 * (n - i) * Mathf.Pow(1 - t, n - i - 1) * Mathf.Pow(t, i) * Cn_m(n, i) * pi + i * Mathf.Pow(1 - t, n - i) * Mathf.Pow(t, i - 1) * Cn_m(n, i) * pi);
}
return ret;
}
// 获取指定点上的法向量偏移
private Vector3 GetBendNormalVector(float t, Vector3 oriPos, Vector3 bendVector)
{
Vector3 tangentVector = CalculateBezierTangent(t);//切线斜率
//切线竖直时,顶点在在弯曲向量上的投影向量即为法向量
if(IsEqualZero(tangentVector.x) == true && IsEqualZero(tangentVector.z) == true)
{
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;
}
//浮点判断是否为零
private bool IsEqualZero(float value)
{
return Mathf.Abs(value) < 1e-5;
}
//组合数
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;
}
/************************************根据受力情况计算控制点坐标(旋转)*****************************/
private void UpdateControlPoint()
{
float handleForce = forceItem.force;
//根据受力计算各个控制点旋转角度
for(int i = 1; i <= cpArr.Length - 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 cp = cpArr[i].gameObject.GetComponent<ControlPoint>();
float rotateRate = Mathf.Clamp(handleForce / cp.bendForce, 0f, 1.0f);
//设置旋转角度
cpArr[i].localRotation = Quaternion.Lerp(Quaternion.Euler(0, 0, 0), maxRotation, rotateRate);
}
}
}
SimpleMesh
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SimpleMesh : MonoBehaviour
{
public float meshWidth = 1f;
public float meshHeight = 10f;
public int phaseCount = 20;
public float endWidth = 0.1f;
void Awake()
{
float phaseHeight = meshHeight / (phaseCount - 1);
float decreaseWidth = (meshWidth - endWidth) / (phaseCount - 1);
// 顶点
Vector3[] vertices = new Vector3[phaseCount * 6];
float bottomY = -meshHeight * 0.5f;
for(int i = 0; i < phaseCount; i++)
{
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);
}
//法线
Vector3[] normals = new Vector3[phaseCount * 6];
for(int i = 0; i < phaseCount; i++)
{
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;
}
//三角形
int[] indices = new int[(phaseCount - 1) * 6 * 6];
for( int i = 1; i < phaseCount; i++)
{
for(int j = 0; j < 6; j++)
{
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;
}
}
ForceItem
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ForceItem : MonoBehaviour
{
[Range(0, 100f)]
public float force = 10;
void Update()
{
if(Input.GetKey(KeyCode.W))
{
transform.position += new Vector3(0, 0.03f, 0);
}
else if(Input.GetKey(KeyCode.S))
{
transform.position -= new Vector3(0, 0.03f, 0);
}
if(Input.GetKey(KeyCode.D))
{
transform.position += new Vector3(0, 0, 0.03f);
}
else if(Input.GetKey(KeyCode.A))
{
transform.position -= new Vector3(0, 0, 0.03f);
}
}
}
ControlPoint
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ControlPoint : MonoBehaviour
{
public float bendForce = 10f; //该节点完全弯曲所需要的力
}
最后
该方法做出来的弯曲效果还是很自然的,使用也比较简单,且不需要关节控制。但是比较吃性能,可以考虑在ComputeShader里面实现。另外考虑到光照,顶点坐标更新后需要重新计算下mesh的法线信息normals。