Bootstrap

物体网格弹性变形---Unity中实现

在游戏引擎场景中的3D物体是由一定数量的点、面组成的,如下图:

要使这些物体变形就是改变3D物体每个顶点状态。

1.首先在Unity场景中增加一个球体,如下图

3D组件默认拥有MeshFilter、meshRenderer、Collider组件,分别用来获取Mesh顶点、渲染物体、返回射线碰撞位置信息

新建物体形变脚本MeshDeformer,并在游戏开始时缓存形变的网格和顶点信息,新建完成后,将脚本挂载到要形变的物体上。

public class MeshDeformer : MonoBehaviour
{
    //需要变形的mesh网格
    Mesh deformingMesh;
    //顶点原始位置,移动后的顶点位置
    Vector3[] originalVertices, displacedVertices;

    void Start()
    {
        //获取变形网格
        deformingMesh = GetComponent<MeshFilter>().mesh;
        //获取变形网格的所有顶点位置
        originalVertices = deformingMesh.vertices;
        displacedVertices = new Vector3[originalVertices.Length];
        for (int i = 0; i < originalVertices.Length; i++)
        {
            displacedVertices[i] = originalVertices[i];
        }
    }
}

新建输入脚本MeshFormerInput,并在Update函数中检测输入

public class MeshDeformerInput : MonoBehaviour
{

    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            HandleInput();
        }
    }

    void HandleInput()
    {
        //获得从相机位置往鼠标点击屏幕点方向的射线
        Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(inputRay, out hit))
        {
            MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();
            if (deformer)
            {
                Debug.Log(deformer);
            }
        }
    }
}

 如果一切顺利则,在场景中点击物体就会在Unity控制台中打印获取到的组件信息

在MeshDeformer脚本中增加施加力的作用效果的方法,

    /// <summary>
    /// 给形变物体施加力
    /// </summary>
    /// <param name="point"></param>
    /// <param name="force"></param>
    public void AddDeformingForce(Vector3 point, float force)
    {
           for (int i = 0; i < displacedVertices.Length; i++)
           {
               //施加力到顶点
               AddForceToVertex(i, point, force);
           }
    }

    /// <summary>
    /// 给某个顶点添加力,将力转化为顶点的速度
    /// </summary>
    /// <param name="i"></param>
    /// <param name="point"></param>
    /// <param name="force"></param>
    void AddForceToVertex(int i, Vector3 point, float force)
    {
     
    }

网格变形是因为对其每个顶点施加了力。当顶点被推时,它们会获得速度。随着时间的推移,顶点都会改变它们的位置。如果所有顶点都受到完全相同的力,则整个对象将移动而不改变其形状,所以我们需要知道每个顶点的变形力的方向和距离,两者都可以从指向力点到顶点位置的向量中导出。使用平方反比定律找到衰减的力,只需将原始力除以距离的平方,就可以得到衰减的力。 

    /// <summary>
    /// 给某个顶点添加力,将力转化为顶点的速度
    /// </summary>
    /// <param name="i"></param>
    /// <param name="point"></param>
    /// <param name="force"></param>
    void AddForceToVertex(int i, Vector3 point, float force)
    {
        //计算施加的力
        Vector3 pointToVertex = displacedVertices[i] - point;
        //实际上,如果只用F/d*d,再d=0时,衰减的力会变成无穷大,所以除以 1 加上距离的平方,保证了当距离为零时力处于全强度状态。
        float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
        float velocity = attenuatedForce * Time.deltaTime;
        //a = F/m,忽略每个质点的质量,将质量都设为1,则dv = Fdt
        vertexVelocities[i] += pointToVertex.normalized * velocity;
    }

计算了每个顶点的速度接下来,在MeshDeformer脚本的Update方法中移动顶点

   void Update()
   {
       for (int i = 0; i < displacedVertices.Length; i++)
       {
           UpdateVertex(i);
       }
       deformingMesh.vertices = displacedVertices;
       deformingMesh.RecalculateNormals();
   }

   /// <summary>
   /// 更新顶点
   /// </summary>
   /// <param name="i"></param>
   void UpdateVertex(int i)
   {
       Vector3 velocity = vertexVelocities[i];
	   displacedVertices[i] += velocity * Time.deltaTime;
   }

一切顺利会得到以下效果: 

增加弹力和阻尼:在MeshFormer.cs中的UpdateVertex中增加弹力和阻尼的计算

   /// <summary>
   /// 更新顶点
   /// </summary>
   /// <param name="i"></param>
   void UpdateVertex(int i)
   {
       Vector3 velocity = vertexVelocities[i];
       //胡克定律 F = -kx,k是常数,是物体的劲度系数(倔强系数)(弹性系数)x是弹簧的伸长量(或压缩量)
       //x = displacedVertices[i] - originalVertices[i]
       //F = -springForce * displacement; 
       Vector3 displacement = displacedVertices[i] - originalVertices[i]; 
       velocity -= displacement * springForce * Time.deltaTime;
       vertexVelocities[i] = velocity;
       //通过不断减慢顶点的速度来防止这种永恒的振荡。此阻尼效果可替代阻力、阻力、惯性等
       //阻尼越高,对象的弹性就越小,看起来越迟缓。
       //v = velocity(1-damping)
       velocity *= 1f - damping * Time.deltaTime;
       displacedVertices[i] += velocity * Time.deltaTime;
   }

最后处理:

现在的变形体是放在原点的,而变形体的顶点坐标都是模型坐标系的本地坐标,我们通过射线碰碰撞得到的着力点是在世界坐标系下,因此我们需要将二者变换到同一坐标系下进行力的计算。

   /// <summary>
   /// 给形变物体施加力
   /// </summary>
   /// <param name="point"></param>
   /// <param name="force"></param>
   public void AddDeformingForce(Vector3 point, float force)
   {
       point = transform.InverseTransformPoint(point);
       Debug.DrawLine(Camera.main.transform.position, point);
       for (int i = 0; i < displacedVertices.Length; i++)
       {
           AddForceToVertex(i, point, force);
       }
   }

物体放缩后,顶点之间的距离会相应的变大或者缩小,如下图:一个球体没有放大和放大两倍的时候的顶点位置。

由上在变形体放缩后需要调整一下每两个顶点之间的作用力,否则用平方反比的计算出来的力,在不同的放缩下,大小会有不同,因此,需要变化三个地方,一个是施加在顶点上的力需要放缩,一个是相互作用力的距离计算时需要放缩,最后一个是顶点移动的距离需要放缩。

最终效果:

完整代码:

public class MeshDeformerInput : MonoBehaviour
{
    //施加的力
    public float force = 10f;
    //力的偏移
    public float forceOffset = 0.1f;

    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            HandleInput();
        }
    }

    void HandleInput()
    {
        //获得从相机位置往鼠标点击屏幕点方向的射线
        Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(inputRay, out hit))
        {
            MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();
            Debug.Log(deformer);
            if (deformer)
            {
                Vector3 point = hit.point;
                point += hit.normal * forceOffset;
                deformer.AddDeformingForce(point, force);
 
            }
        }
    }
}
public class MeshDeformer : MonoBehaviour
{
    //需要变形的mesh网格
    Mesh deformingMesh;
    //顶点原始位置,移动后的顶点位置
    Vector3[] originalVertices, displacedVertices;
    //顶点的速度
    Vector3[] vertexVelocities;

    //弹力
    public float springForce = 20f;
    //阻尼
    public float damping = 5f;

    //放缩比例
    float uniformScale = 1f;

    void Start()
    {
        //获取变形网格
        deformingMesh = GetComponent<MeshFilter>().mesh;
        //获取变形网格的所有顶点位置
        originalVertices = deformingMesh.vertices;
        displacedVertices = new Vector3[originalVertices.Length];
        for (int i = 0; i < originalVertices.Length; i++)
        {
            displacedVertices[i] = originalVertices[i];
        }
        vertexVelocities = new Vector3[originalVertices.Length];
    }

    /// <summary>
    /// 给形变物体施加力
    /// </summary>
    /// <param name="point"></param>
    /// <param name="force"></param>
    public void AddDeformingForce(Vector3 point, float force)
    {
        point = transform.InverseTransformPoint(point);
        Debug.DrawLine(Camera.main.transform.position, point);
        for (int i = 0; i < displacedVertices.Length; i++)
        {
            AddForceToVertex(i, point, force);
        }
    }

    /// <summary>
    /// 给某个顶点添加力,将力转化为顶点的速度
    /// </summary>
    /// <param name="i"></param>
    /// <param name="point"></param>
    /// <param name="force"></param>
    void AddForceToVertex(int i, Vector3 point, float force)
    {
        //计算施加力的方向
        Vector3 pointToVertex = displacedVertices[i] - point;
        pointToVertex *= uniformScale;
        //实际上,如果只用F/d*d,再d=0时,衰减的力会变成无穷大,所以除以 1 加上距离的平方,保证了当距离为零时力处于全强度状态。
        float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
        float velocity = attenuatedForce * Time.deltaTime;
        //a = F/m,忽略每个质点的质量,将质量都设为1,则dv = Fdt
        vertexVelocities[i] += pointToVertex.normalized * velocity;
    }

    void Update()
    {
        uniformScale = this.transform.localScale.x;
        for (int i = 0; i < displacedVertices.Length; i++)
        {
            UpdateVertex(i);
        }
        deformingMesh.vertices = displacedVertices;
        deformingMesh.RecalculateNormals();
    }

    /// <summary>
    /// 更新顶点
    /// </summary>
    /// <param name="i"></param>
    void UpdateVertex(int i)
    {
        Vector3 velocity = vertexVelocities[i];
        //胡克定律 F = -kx,k是常数,是物体的劲度系数(倔强系数)(弹性系数)x是弹簧的伸长量(或压缩量)
        //x = displacedVertices[i] - originalVertices[i]
        //F = -springForce * displacement; 
        Vector3 displacement = displacedVertices[i] - originalVertices[i];
        displacement *= uniformScale;
        velocity -= displacement * springForce * Time.deltaTime;
        vertexVelocities[i] = velocity;
        //通过不断减慢顶点的速度来防止这种永恒的振荡。此阻尼效果可替代阻力、阻力、惯性等
        //阻尼越高,对象的弹性就越小,看起来越迟缓。
        //v = velocity(1-damping)
        velocity *= 1f - damping * Time.deltaTime;
        displacedVertices[i] += velocity * Time.deltaTime / uniformScale;
    }
}

参考链接:

网格变形,Unity C# 教程 (catlikecoding.com)

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;