昨天车被撞了,明天要打第三针疫苗,下周还要修改搭建新项目的框架。
实属是没时间了,而且武汉公司都挺佛系的,一到下班楼都空了,保持了四年多不加班的记录不能破。所以暂时直接使用unity自带的joint关节完成了线缆的功能。
需求构建一个支持物理属性的线缆绳索功能,如下:
我们想象P0-P7为刚体骨骼结点,一根绳索由n截刚性线段组成,首先构建这种结构,如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum EBoneAlignType
{
Front,
Center,
Back
}
public enum EBoneAxisType
{
X,
Y,
Z
}
public class ElasticRigidBoneChain : MonoBehaviour
{
[Header("更新骨骼链条")]
public bool isUpdate = false;
[Header("骨骼数量")]
public int boneCount = 100;
[Header("骨骼长度")]
public float boneLength = 1f;
[Header("骨链对齐端点")]
public EBoneAlignType boneAlignType = EBoneAlignType.Center;
[Header("骨链朝向")]
public EAxisType boneAxisType = EAxisType.X;
[Header("骨节预制件")]
public ABoneNodeBase boneNodePrefab;
private Queue<ABoneNodeBase> boneNodeQueue = new Queue<ABoneNodeBase>();
private List<ABoneNodeBase> boneNodeList = new List<ABoneNodeBase>();
private List<Vector3> boneNodePosList = new List<Vector3>();
private ChainRopeMeshSkin chainSkin;
private void Awake()
{
chainSkin = GetComponent<ChainRopeMeshSkin>();
}
void Start()
{
UpdateBoneChain();
}
/// <summary>
/// 更新骨骼链条
/// </summary>
private void UpdateBoneChain()
{
float offset = 0f;
float length = boneCount * boneLength;
switch (boneAlignType)
{
case EBoneAlignType.Front:
break;
case EBoneAlignType.Center:
offset = (-length / 2f);
break;
case EBoneAlignType.Back:
offset = -length;
break;
}
boneNodeList.Clear();
boneNodePosList.Clear();
for (int i = 0; i < boneCount; i++)
{
Vector3 npos = Vector3.zero;
switch (boneAxisType)
{
case EAxisType.X:
npos = new Vector3(i * boneLength + offset, 0, 0);
break;
case EAxisType.Y:
npos = new Vector3(0, i * boneLength + offset, 0);
break;
case EAxisType.Z:
npos = new Vector3(0, 0, i * boneLength + offset);
break;
}
ABoneNodeBase bnode = AllocBoneNode();
bnode.Initial(this, npos, true);
boneNodeList.Add(bnode);
boneNodePosList.Add(npos);
}
}
/// <summary>
/// 清理骨骼链条
/// </summary>
private void ClearBoneChain()
{
for (int i = 0; i < boneNodeList.Count; i++)
{
ABoneNodeBase bnode = boneNodeList[i];
RecycleBoneNode(bnode);
}
boneNodeList.Clear();
boneNodePosList.Clear();
}
#region ///bonenode factory
/// <summary>
/// 生成一个骨骼节点
/// </summary>
/// <returns></returns>
private ABoneNodeBase AllocBoneNode()
{
ABoneNodeBase bnode = null;
if (boneNodeQueue.Count > 0)
{
bnode = boneNodeQueue.Dequeue();
}
if (bnode == null)
{
bnode = GameObject.Instantiate<ABoneNodeBase>(boneNodePrefab);
}
bnode.gameObject.SetActive(true);
return bnode;
}
/// <summary>
/// 回收一个骨骼节点
/// </summary>
/// <param name="bnode"></param>
private void RecycleBoneNode(ABoneNodeBase bnode)
{
if (bnode != null)
{
bnode.gameObject.SetActive(false);
boneNodeQueue.Enqueue(bnode);
}
}
#endregion
void Update()
{
#if UNITY_EDITOR
for (int i = 0; i < (boneNodeList.Count - 1); i++)
{
ABoneNodeBase fbnode = boneNodeList[i];
ABoneNodeBase tbnode = boneNodeList[i + 1];
Debug.DrawLine(fbnode.GetWorldPos(), tbnode.GetWorldPos(), Color.black);
}
#endif
if (isUpdate)
{
ClearBoneChain();
UpdateBoneChain();
isUpdate = false;
}
}
}
骨骼节点基类代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum EBoneNodeType
{
Sphere,
Cube,
}
public abstract class ABoneNodeBase : MonoBehaviour
{
[Header("骨节初始坐标")]
public Vector3 origNodePos;
[Header("是否可编辑")]
public bool isNodeEdit = true;
[Header("骨节类型")]
protected EBoneNodeType nodeType;
[Header("骨节大小")]
[Range(0.01f, 1f)]
public float boneNodeSize = 0.1f;
protected ElasticRigidBoneChain boneChain;
protected virtual void Awake()
{
}
public virtual void Initial(ElasticRigidBoneChain bc, Vector3 pos, bool edit)
{
boneChain = bc;
origNodePos = pos;
isNodeEdit = edit;
}
protected virtual void Start()
{
}
public virtual Vector3 GetWorldPos()
{
return transform.position;
}
public virtual Vector3 GetLocalPos()
{
return transform.localPosition;
}
protected virtual void Update()
{
}
protected virtual void OnDestroy()
{
}
}
效果如下:
接下来给每个骨骼节点添加joint组件,当然首节点和尾结固定坐标,作为约束节点,如下:
//配置动态关节信息
ABoneNodeBase startbnode = boneNodeList[0];
startbnode.SetConstrain(RigidbodyConstraints.FreezePosition);
startbnode.SetKinematic(false);
for (int i = 1; i < boneCount; i++)
{
ABoneNodeBase prevbnode = boneNodeList[i - 1];
ABoneNodeBase jointbnode = boneNodeList[i];
jointbnode.SetJoint(EJointType.Fixed, prevbnode.rigid);
}
ABoneNodeBase endbnode = boneNodeList[boneCount - 1];
endbnode.SetConstrain(RigidbodyConstraints.FreezePosition);
骨骼节点添加Joint功能:
public enum EJointType
{
Hinge, //链条
Fixed, //固定
Spring, //弹簧
Chara, //角色
Config, //配置
}
#region ///物理关节
public virtual void SetGravity(bool enab)
{
rigid.useGravity = enab;
}
public virtual void SetKinematic(bool enab)
{
rigid.isKinematic = enab;
}
public virtual void SetConstrain(RigidbodyConstraints cst)
{
rigid.constraints = cst;
}
/// <summary>
/// 设置关节
/// </summary>
/// <param name="jtype"></param>
/// <param name="ctrigid"></param>
public virtual void SetJoint(EJointType jtype, Rigidbody ctrigid)
{
jointType = jtype;
switch (jtype)
{
case EJointType.Hinge:
{
joint = gameObject.AddComponent<HingeJoint>();
}
break;
case EJointType.Fixed:
{
joint = gameObject.AddComponent<FixedJoint>();
}
break;
case EJointType.Spring:
{
joint = gameObject.AddComponent<SpringJoint>();
}
break;
case EJointType.Chara:
{
joint = gameObject.AddComponent<CharacterJoint>();
}
break;
case EJointType.Config:
{
joint = gameObject.AddComponent<ConfigurableJoint>();
}
break;
}
joint.connectedBody = ctrigid;
rigid.useGravity = true;
rigid.isKinematic = false;
}
/// <summary>
/// 重置关节
/// </summary>
public virtual void ResetJoint()
{
Joint.Destroy(joint);
rigid.useGravity = false;
rigid.isKinematic = true;
}
#endregion
效果如下:
物理效果勉强能凑合用,当然unity提供的joint配置参数也可以调整到适合的效果(当然我调不出来)。
接下来就开始给这个骨骼节点链条进行网格蒙皮,这里建议先看关于圆柱体网格构建的讲解。
而这里的绳索网格可以由圆柱体衍生出来,如下:
可以把绳索结构想象成n段圆柱体连接而成,那么我们必须重新构建网格顶点、法向量、三角数据,如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class ChainRopeMeshSkin : MonoBehaviour
{
[Range(0, 2f)]
public float circleRadius = 0.1f;
[Range(3, 50)]
public int circleSegement = 20;
[Range(0, 90f)]
public float rotateAngle = 30f;
private MeshRenderer meshRender;
private MeshFilter meshFilter;
private Mesh mesh;
private bool isInited = false;
private List<ABoneNodeBase> boneNodeList = new List<ABoneNodeBase>();
private Vector3[] buildBonePoses;
private void Awake()
{
meshRender = GetComponent<MeshRenderer>();
meshFilter = GetComponent<MeshFilter>();
mesh = new Mesh();
}
void Start()
{
}
public void InitParams(List<ABoneNodeBase> bonelist)
{
isInited = true;
boneNodeList = bonelist;
BuildRopeMesh();
}
private void Update()
{
if (isInited)
{
if (CheckRequestRebuild())
{
RebuildRopeMesh();
}
}
}
#region ///重构建判断
/// <summary>
/// 获取bonepos数组
/// </summary>
/// <returns></returns>
private Vector3[] GetBoneNodePosArray()
{
Vector3[] poses = new Vector3[boneNodeList.Count];
for (int i = 0; i < boneNodeList.Count; i++)
{
poses[i] = boneNodeList[i].GetWorldPos();
}
return poses;
}
/// <summary>
/// 判断是否需要重建网格
/// </summary>
/// <returns></returns>
private bool CheckRequestRebuild()
{
Vector3[] poses = GetBoneNodePosArray();
for (int i = 0; i < poses.Length; i++)
{
Vector3 a = poses[i];
Vector3 b = buildBonePoses[i];
if (!CheckVector3Approximate(a, b))
{
return true;
}
}
return false;
}
private bool CheckVector3Approximate(Vector3 a, Vector3 b)
{
if (!Mathf.Approximately(a.x, b.x)
|| !Mathf.Approximately(a.y, b.y)
|| !Mathf.Approximately(a.z, b.z))
{
return false;
}
return true;
}
#endregion
/// <summary>
/// 构建绳索网格
/// 初始构建一次
/// </summary>
public void BuildRopeMesh()
{
//小于3个骨骼节点
//就不满足关节链条的需求
if (boneNodeList.Count < 3)
{
#if UNITY_EDITOR
Debug.LogErrorFormat("ChainRopeMeshSkin UpdateRopeMesh boneNodeList.Count = {0}", boneNodeList.Count);
#endif
isInited = false;
return;
}
CreateMeshVertices();
CreateMeshNormals();
CreateMeshTriangles();
meshFilter.sharedMesh = mesh;
//记录当前构建的boneposes
//用于动态运动后的重建
buildBonePoses = GetBoneNodePosArray();
}
/// <summary>
/// 重构建绳索网格
/// 动态运动后重构
/// 只重构顶点和法向量
/// </summary>
public void RebuildRopeMesh()
{
CreateMeshVertices();
CreateMeshNormals();
buildBonePoses = GetBoneNodePosArray();
}
/// <summary>
/// 创建mesh vertices
/// </summary>
private void CreateMeshVertices()
{
int ncount = boneNodeList.Count;
List<Vector3> vertlist = new List<Vector3>();
Vector3 fpos = boneNodeList[0].GetWorldPos();
vertlist.Add(fpos);
//依次圆柱体起点切面
for (int i = 0; i < ncount - 1; i++)
{
Vector3 start = boneNodeList[i].GetWorldPos();
Vector3 end = boneNodeList[i + 1].GetWorldPos();
Vector3[] sposarr = CalculateCirclePoints(start, end);
if (i == 0)
{
vertlist.AddRange(sposarr);
}
vertlist.AddRange(sposarr);
if (i == (ncount - 2))
{
Vector3[] eposarr = CalculateBiasPoints(start, end, sposarr);
vertlist.AddRange(eposarr);
vertlist.AddRange(eposarr);
}
}
Vector3 tpos = boneNodeList[ncount - 1].GetWorldPos();
vertlist.Add(tpos);
mesh.vertices = vertlist.ToArray();
}
/// <summary>
/// 创建mesh normals
/// </summary>
private void CreateMeshNormals()
{
List<Vector3> normlist = new List<Vector3>();
int ncount = boneNodeList.Count;
//起始面
Vector3 nf = (boneNodeList[0].GetWorldPos() - boneNodeList[1].GetWorldPos()).normalized;
Vector3[] nfs = new Vector3[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
nfs[i] = nf;
}
normlist.Add(nf);
normlist.AddRange(nfs);
//圆柱体截面
for (int i = 0; i < ncount - 1; i++)
{
Vector3 start = boneNodeList[i].GetWorldPos();
Vector3 end = boneNodeList[i + 1].GetWorldPos();
Vector3[] sposarr = CalculateCirclePoints(start, end);
Vector3[] nms = new Vector3[circleSegement];
for (int k = 0; k < circleSegement; k++)
{
nms[k] = (sposarr[k] - start).normalized;
}
normlist.AddRange(nms);
if (i == (ncount - 2))
{
Vector3[] eposarr = CalculateBiasPoints(start, end, sposarr);
for (int k = 0; k < circleSegement; k++)
{
nms[k] = (eposarr[k] - end).normalized;
}
normlist.AddRange(nms);
}
}
//终点面
Vector3 nt = (boneNodeList[ncount - 1].GetWorldPos() - boneNodeList[ncount - 2].GetWorldPos()).normalized;
Vector3[] nts = new Vector3[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
nts[i] = nt;
}
normlist.AddRange(nts);
normlist.Add(nt);
mesh.normals = normlist.ToArray();
}
/// <summary>
/// 创建mesh triangles
/// </summary>
private void CreateMeshTriangles()
{
int ncount = boneNodeList.Count;
List<int> trilist = new List<int>();
//起点圆
int startindex = 0; //起始点索引
for (int i = 0; i < circleSegement; i++)
{
int[] tris = new int[]
{
startindex,
i+2>circleSegement?(i+2)%circleSegement:i+2,
i+1
};
trilist.AddRange(tris);
}
//中间截面
for (int i = 0; i < (ncount - 1); i++)
{
int findex = (i + 1) * circleSegement + 1; //起点界面开始索引
int tindex = (i + 2) * circleSegement + 1; //终点界面开始索引
for (int k = 0; k < circleSegement; k++)
{
int[] tris = new int[]
{
findex+k,
tindex+k+1>(tindex+circleSegement-1)?tindex:tindex+k+1,
tindex+k,
};
trilist.AddRange(tris);
tris = new int[]
{
findex+k,
findex+k+1>(findex+circleSegement-1)?findex:findex+k+1,
tindex+k+1>(tindex+circleSegement-1)?tindex:tindex+k+1,
};
trilist.AddRange(tris);
}
}
//终点圆
int endindex = (ncount + 2) * circleSegement + 1; //终止点索引
int eindex = (ncount + 1) * circleSegement + 1; //终点圆起点索引
for (int i = 0; i < circleSegement; i++)
{
int[] tris = new int[]
{
endindex,
eindex+i,
eindex+i+1>(eindex+circleSegement-1)?eindex:eindex+i+1
};
trilist.AddRange(tris);
}
mesh.triangles = trilist.ToArray();
}
/// <summary>
/// 创建mesh uvs
/// </summary>
private void CreateMeshUVs()
{
List<Vector2> uvlist = new List<Vector2>();
mesh.uv = uvlist.ToArray();
}
/// <summary>
/// 清理绳索网格
/// </summary>
public void ClearRopeMesh()
{
isInited = false;
mesh.Clear();
meshFilter.sharedMesh = null;
boneNodeList.Clear();
buildBonePoses = null;
}
#region ///计算空间圆参数
/// <summary>
/// 计算start起点
/// end终点
/// 空间圆坐标数组
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <returns></returns>
private Vector3[] CalculateCirclePoints(Vector3 start, Vector3 end)
{
Vector3 p2 = RotateAroundMatchAxis(start, end, rotateAngle * Mathf.Deg2Rad);
Vector3 p1 = RayLineCrossPanel(start, end, p2);
Vector3 p = start + (p1 - start).normalized * circleRadius;
Vector3[] posarr = new Vector3[circleSegement];
posarr[0] = p;
Vector3 naxis = (end - start).normalized;
float segerad = 2f * Mathf.PI / (float)circleSegement;
for (int i = 1; i < circleSegement; i++)
{
float rad = segerad * i;
Vector3 segepos = RotateAroundAnyAxis(start, p, naxis, rad);
posarr[i] = segepos;
}
return posarr;
}
/// <summary>
/// 计算出start的空间圆坐标
/// 根据end终点偏移出坐标数组
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="sposarr"></param>
/// <returns></returns>
private Vector3[] CalculateBiasPoints(Vector3 start, Vector3 end, Vector3[] sposarr)
{
Vector3[] eposarr = new Vector3[sposarr.Length];
Vector3 offset = end - start;
for (int i = 0; i < sposarr.Length; i++)
{
Vector3 spos = sposarr[i];
Vector3 epos = spos + offset;
eposarr[i] = epos;
}
return eposarr;
}
/// <summary>
/// p(x,y,z)点绕start为起点的任意坐标轴旋转后的坐标
/// </summary>
/// <param name="start"></param>
/// <param name="naxis"></param>
/// <param name="rad"></param>
/// <returns></returns>
private Vector3 RotateAroundAnyAxis(Vector3 start, Vector3 p, Vector3 naxis, float rad)
{
float n1 = naxis.x;
float n2 = naxis.y;
float n3 = naxis.z;
//获取p相对start的本地坐标
p -= start;
float sin = Mathf.Sin(rad);
float cos = Mathf.Cos(rad);
Matrix3x3 mat = new Matrix3x3();
mat.m00 = n1 * n1 * (1 - cos) + cos;
mat.m01 = n1 * n2 * (1 - cos) - n3 * sin;
mat.m02 = n1 * n3 * (1 - cos) + n2 * sin;
mat.m10 = n1 * n2 * (1 - cos) + n3 * sin;
mat.m11 = n2 * n2 * (1 - cos) + cos;
mat.m12 = n2 * n3 * (1 - cos) - n1 * sin;
mat.m20 = n1 * n3 * (1 - cos) - n2 * sin;
mat.m21 = n2 * n3 * (1 - cos) + n1 * sin;
mat.m22 = n3 * n3 * (1 - cos) + cos;
//绕轴旋转后,处理成世界坐标
Vector3 px = mat * p + start;
return px;
}
/// <summary>
/// 通过start end计算start所处平面F方程
/// 通过end p2计算射线与平面F交点p1
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="p2"></param>
/// <returns></returns>
private Vector3 RayLineCrossPanel(Vector3 start, Vector3 end, Vector3 p2)
{
//start = from
//end = to
//构建平面F方程参数
Vector3 ft = end - start;
float u = ft.x, v = ft.y, w = ft.z;
float a = start.x, b = start.y, c = start.z;
//构建射线tp2参数
float sx = end.x;
float sy = end.y;
float sz = end.z;
Vector3 ntp2 = (p2 - end).normalized;
float dx = ntp2.x;
float dy = ntp2.y;
float dz = ntp2.z;
//计算p1
float n = ((u * a + v * b + w * c) - (u * sx + v * sy + w * sz)) / (u * dx + v * dy + w * dz);
Vector3 p1 = end + n * ntp2;
return p1;
}
/// <summary>
/// 根据end->start单位向量朝向
/// 选择性根据xyz轴旋转
/// 避免ret和start共点
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="rad"></param>
/// <returns></returns>
private Vector3 RotateAroundMatchAxis(Vector3 start, Vector3 end, float rad)
{
Vector3 dir = (start - end).normalized;
Vector3 ret;
if (CheckVector3Approximate(dir, Vector3.right) || CheckVector3Approximate(dir, Vector3.left))
{
ret = RotateAroundYAxis(start, end, rad);
return ret;
}
if (CheckVector3Approximate(dir, Vector3.up) || CheckVector3Approximate(dir, Vector3.down))
{
ret = RotateAroundZAxis(start, end, rad);
return ret;
}
if (CheckVector3Approximate(dir, Vector3.forward) || CheckVector3Approximate(dir, Vector3.back))
{
ret = RotateAroundXAxis(start, end, rad);
return ret;
}
ret = RotateAroundXAxis(start, end, rad);
return ret;
}
private Vector3 RotateAroundXAxis(Vector3 start, Vector3 end, float rad)
{
Matrix3x3 mat = new Matrix3x3();
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
mat.m00 = 1;
mat.m01 = 0;
mat.m02 = 0;
mat.m10 = 0;
mat.m11 = cos;
mat.m12 = -sin;
mat.m20 = 0;
mat.m21 = sin;
mat.m22 = cos;
Vector3 ret = mat * (start - end) + end;
return ret;
}
private Vector3 RotateAroundYAxis(Vector3 start, Vector3 end, float rad)
{
Matrix3x3 mat = new Matrix3x3();
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
mat.m00 = cos;
mat.m01 = 0;
mat.m02 = sin;
mat.m10 = 0;
mat.m11 = 1;
mat.m12 = 0;
mat.m20 = -sin;
mat.m21 = 0;
mat.m22 = cos;
Vector3 ret = mat * (start - end) + end;
return ret;
}
private Vector3 RotateAroundZAxis(Vector3 start, Vector3 end, float rad)
{
Matrix3x3 mat = new Matrix3x3();
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
mat.m00 = cos;
mat.m01 = -sin;
mat.m02 = 0;
mat.m10 = sin;
mat.m11 = cos;
mat.m12 = 0;
mat.m20 = 0;
mat.m21 = 0;
mat.m22 = 1;
Vector3 ret = mat * (start - end) + end;
return ret;
}
#endregion
}
这里顺便修改了上一篇没注意的问题:那就是如果start-end单位法向量与x、y、z轴重合,那么需要改成不同的轴向旋转,不然得到的结果与start共点,那就无法计算出空间圆。
同时我把Vertices、Normals、Triangles、UVs分离计算,一方面是为了方便理解,另一方面是重建网格只需要重建Vertices、Normals(UVs可以不重建,因为我使用的固定长度骨骼节点),所以分离开用于重构建可以稍微节省一点计算量。
这里我们还可以将Vertices、Normals的重建合并,如下:
/// <summary>
/// 创建mesh vertices和normals
/// </summary>
private void CreateMeshVerticesAndNormals()
{
int ncount = boneNodeList.Count;
List<Vector3> vertlist = new List<Vector3>();
List<Vector3> normlist = new List<Vector3>();
//起始面
Vector3 fpos = boneNodeList[0].GetWorldPos();
vertlist.Add(fpos);
Vector3 nf = (boneNodeList[0].GetWorldPos() - boneNodeList[1].GetWorldPos()).normalized;
Vector3[] nfs = new Vector3[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
nfs[i] = nf;
}
normlist.Add(nf);
normlist.AddRange(nfs);
//中间截面
for (int i = 0; i < ncount - 1; i++)
{
Vector3 start = boneNodeList[i].GetWorldPos();
Vector3 end = boneNodeList[i + 1].GetWorldPos();
Vector3[] sposarr = CalculateCirclePoints(start, end);
if (i == 0)
{
vertlist.AddRange(sposarr);
}
vertlist.AddRange(sposarr);
if (i == (ncount - 2))
{
Vector3[] eposarr = CalculateBiasPoints(start, end, sposarr);
vertlist.AddRange(eposarr);
vertlist.AddRange(eposarr);
}
Vector3[] nms = new Vector3[circleSegement];
for (int k = 0; k < circleSegement; k++)
{
nms[k] = (sposarr[k] - start).normalized;
}
normlist.AddRange(nms);
if (i == (ncount - 2))
{
Vector3[] eposarr = CalculateBiasPoints(start, end, sposarr);
for (int k = 0; k < circleSegement; k++)
{
nms[k] = (eposarr[k] - end).normalized;
}
normlist.AddRange(nms);
}
}
//终点面
Vector3 tpos = boneNodeList[ncount - 1].GetWorldPos();
vertlist.Add(tpos);
Vector3 nt = (boneNodeList[ncount - 1].GetWorldPos() - boneNodeList[ncount - 2].GetWorldPos()).normalized;
Vector3[] nts = new Vector3[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
nts[i] = nt;
}
normlist.AddRange(nts);
normlist.Add(nt);
mesh.vertices = vertlist.ToArray();
mesh.normals = normlist.ToArray();
}
稍微节省了那么一点运算性能,效果如下:
接下来构建uv映射计算,如下:
我们将绳索拆分成两端的圆形,对应start、end白色圆形,中间的展开矩形截面,对应123456789黑色矩形。
接下来进行uv映射的计算,如下:
/// <summary>
/// 创建mesh uvs
/// </summary>
private void CreateMeshUVs()
{
float segrad = 2f * Mathf.PI / (float)circleSegement;
float uvcircleradius = 0.25f;
int ncount = boneNodeList.Count;
List<Vector2> uvlist = new List<Vector2>();
//起点圆
Vector2 suv = new Vector2(0.25f, 0.75f);
uvlist.Add(suv);
Vector2[] suvarr = new Vector2[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
float rad = segrad * i;
suvarr[i] = GetCircleUV(suv, uvcircleradius, rad);
}
uvlist.AddRange(suvarr);
//中间截面
for (int i = 0; i < ncount; i++)
{
Vector2[] muvarr = new Vector2[circleSegement];
for (int k = 0; k < circleSegement; k++)
{
float mu = (float)i / (float)(ncount - 1);
float mv = (float)k / (float)(circleSegement) * 0.5f;
muvarr[k] = new Vector2(mu, mv);
}
uvlist.AddRange(muvarr);
}
//终点圆
Vector2[] tuvarr = new Vector2[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
tuvarr[i] = suvarr[i] + new Vector2(0.5f, 0);
}
uvlist.AddRange(tuvarr);
Vector2 tuv = new Vector2(0.75f, 0.75f);
uvlist.Add(tuv);
mesh.uv = uvlist.ToArray();
}
private Vector2 GetCircleUV(Vector2 center, float radius, float rad)
{
float u = center.x + Mathf.Cos(rad) * radius;
float v = center.y + Mathf.Sin(rad) * radius;
Vector2 uv = new Vector2(u, v);
return uv;
}
效果如下:
可以看得出两个问题:
1.End终点圆形“正反面”错误了,那是因为“正面”朝向Start起点面
2.圆柱体截面有一条“缝隙”,这是因为我们的圆柱体截面的起点和终点不共点,所以uv映射会有一条“缝隙”,如下:
p0和pn中间有个缺口,就导致了uv映射的问题,这里的解决方案就是将p0和pn在vertices计算中处理成共点。
最终修改如下:
处理终点圆面反向
/// <summary>
/// 创建mesh uvs
/// </summary>
private void CreateMeshUVs()
{
float segrad = 2f * Mathf.PI / (float)(circleSegement - 1); //处理p0 pn的uv
float uvcircleradius = 0.25f;
int ncount = boneNodeList.Count;
List<Vector2> uvlist = new List<Vector2>();
//起点圆
Vector2 suv = new Vector2(0.25f, 0.75f);
uvlist.Add(suv);
Vector2[] suvarr = new Vector2[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
float rad = segrad * i;
suvarr[i] = GetCircleUV(suv, uvcircleradius, rad);
}
uvlist.AddRange(suvarr);
//中间截面
for (int i = 0; i < ncount; i++)
{
Vector2[] muvarr = new Vector2[circleSegement];
for (int k = 0; k < circleSegement; k++)
{
float mu = (float)i / (float)(ncount - 1);
float mv = (float)k / (float)circleSegement * 0.5f;
muvarr[k] = new Vector2(mu, mv);
}
uvlist.AddRange(muvarr);
}
//终点圆
Vector2[] tuvarr = new Vector2[circleSegement];
for (int i = 0; i < circleSegement; i++)
{
tuvarr[i] = suvarr[circleSegement - i - 1] + new Vector2(0.5f, 0); //处理end圆uv反向
}
uvlist.AddRange(tuvarr);
Vector2 tuv = new Vector2(0.75f, 0.75f);
uvlist.Add(tuv);
mesh.uv = uvlist.ToArray();
}
处理截面起终点坐标共点
private Vector3[] CalculateCirclePoints(Vector3 start, Vector3 end)
{
Vector3 p2 = RotateAroundMatchAxis(start, end, rotateAngle * Mathf.Deg2Rad);
Vector3 p1 = RayLineCrossPanel(start, end, p2);
Vector3 p = start + (p1 - start).normalized * circleRadius;
Vector3[] posarr = new Vector3[circleSegement];
posarr[0] = p;
Vector3 naxis = (end - start).normalized;
float segerad = 2f * Mathf.PI / (float)(circleSegement - 1); //处理p0 pn共点
for (int i = 1; i < circleSegement; i++)
{
float rad = segerad * i;
Vector3 segepos = RotateAroundAnyAxis(start, p, naxis, rad);
posarr[i] = segepos;
}
return posarr;
}
效果如下:
处理方式就相当简单,只需要rad计算扩展一个单位就行了。
接下来就要完成一下需求的特效了,这也是为什么我要计算uv的原因,不然我连贴图都不需要,就调整颜色值就行了,如下:
Shader "ElasticRope/ElasticRopeElactricUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_MainColor("Color",Color) = (1,1,1,1)
_Speed("Speed",Range(0,2)) = 1
_Pow("Pow",Range(0,500)) = 10
[Toggle]_IsEffect("Effect",int) = 0
[Toggle]_IsInverse("Inverse",int) = 0
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
LOD 100
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainColor;
float _Speed;
float _Pow;
int _IsEffect;
int _IsInverse;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = _MainColor;
if(_IsEffect == 1)
{
if(i.uv.y<=0.5)
{
if(_IsInverse == 0){
i.uv.x += _Time.y*_Speed;
}else{
i.uv.x -= _Time.y*_Speed;
}
float s = sin(i.uv.x*_Pow);
col.a = saturate(s);
}else{
col.a = 0;
}
}else{
col.a = 0;
}
return col;
}
ENDCG
}
}
}
效果如下:
暂时满足需求了,年底赶项目,时间预算不足,后面有时间用质点弹簧模型重写一份,因为unity自带的joint实际使用起来问题真多。