辅助工具:关节绳

      昨天车被撞了,明天要打第三针疫苗,下周还要修改搭建新项目的框架。
      实属是没时间了,而且武汉公司都挺佛系的,一到下班楼都空了,保持了四年多不加班的记录不能破。所以暂时直接使用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实际使用起来问题真多。

上一篇:DAY2 Python认字阶段


下一篇:实验七