【转载】Unity 模拟果冻效果 | 简易弹簧质点系统

718 阅读3分钟

原文链接

Unity 模拟果冻效果 | 简易弹簧质点系统 - 为什么不开大

正文

脚本和材质的参数设定如下

效果演示

弹弹弹.gif

物理原理

完整的质点弹簧系统可以看闫老师的课, 这里简单阐述一下用到的简单质点弹簧, 只需要高中物理基础即可:

ab 是一根没有长度的弹簧, 所以现在看起来是重合的 image.png

a主动点(我们拽着拖动的点), b从动点 (被弹力拖着动的点).

计算 从动点 b 的受力, 胡克定律: F=ksF=ks

  • sb 指向 a 的向量
  • k弹簧劲度系数, 自己设定 image.png

此时已经可以模拟无限长弹簧的运动了, 但由于没有阻力, 所以一旦有了外力(玩家拖动), b 会一直运动停不下来.

为弹簧添加阻力. 弹簧阻力为 b 速度反向乘以 阻力系数(阻尼)

  • 阻尼自己设定 image.png

此时已经可以完全模拟弹簧效果了 v2-403de8aaf96257b3d04812aec2ceb52e_720w.webp

将模型根据世界空间各个顶点 y 坐标, 作为 插值因子, 来决定模型上下各部分该处于何处(更靠近主动点, 还是更靠近从动点)

完整代码

C# 代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MassObj : MonoBehaviour
{
    private Material mat;
    private Vector3 followPos = Vector3.zero; // 从动点位置
    private Vector3 massVelocity = Vector3.zero; // 从动点速度
    public float stiffness = 60f; // 劲度系数
    public float damping = 2f; // 阻尼系数

    private float max, min; // 模型在模型空间最高, 最低点的 y 值
    private void Start()
    {
        mat = GetComponent<MeshRenderer>().sharedMaterial;
        followPos = transform.position; // 虚拟抽象一个从动点

        // 距离轴心的物体空间距离
        max = GetComponent<MeshFilter>().sharedMesh.bounds.max.y;
        min = GetComponent<MeshFilter>().sharedMesh.bounds.min.y;

        mat.SetFloat("_MeshH", max - min);// 模型总高度
    }
    private void Update()
    {
        // 进行一些受力, 加速度, 速度, 路程, 运动 的数值计算
        Vector3 force = GetMainForce(); // 弹力
        force += GetDampingForce(); // 阻力
        massVelocity += force * Time.deltaTime;// 将固定质量为 1, 则 force 数值等于加速度数值
        followPos += massVelocity * Time.deltaTime;// 从动点的移动

        // 为 shader 传入数据
        SetMatData();
    }
    private Vector3 GetMainForce()
    {
        // 胡克定律
        Vector3 forceDir = transform.position - followPos;
        return forceDir * stiffness;
    }
    private Vector3 GetDampingForce()
    {
        return -massVelocity * damping;// 弹簧阻尼
    }
    private void SetMatData()
    {
        mat.SetVector("_MainPos", transform.position);// 主动点
        mat.SetVector("_FollowPos", followPos);// 从动点
        mat.SetFloat("_W_Bottom", transform.position.y + min);// 模型最低点y值
    }
}

Shader 代码

这里使用 surface shader , 它能提供 PBR 光照模型, 主要关注顶点 shader 即可, 放在 unlit 也是一样的

Shader "MassPoint/MassObj"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows addshadow vertex:vert

        struct Input
        {
            float2 uv_MainTex;
        };

        sampler2D _MainTex;
        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        float4 _MainPos, _FollowPos;///< world space
        float _MeshH, _W_Bottom;

        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        void vert (inout appdata_full v, out Input o)
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);

            float3 mainPos = mul(unity_WorldToObject, _MainPos).xyz;///< 主动点在模型空间的位置
            float3 follow = mul(unity_WorldToObject, _FollowPos).xyz;///< 从动点在模型空间的位置
            float3 offDir = follow - mainPos;///< 偏移方向
            float3 followVert = v.vertex.xyz + offDir;///< 从动的模型顶点进行位置偏移
            float3 wPos = mul(unity_ObjectToWorld, v.vertex).xyz;///< 模型的世界坐标
            float mask = (wPos.y - _W_Bottom) / max(0.00001, _MeshH);///< 将模型世界顶点y值, 映射[0, 1], 作为上下的遮罩
            v.vertex.xyz = lerp(v.vertex.xyz, followVert, mask); ///< 用遮罩来插值顶点该的主动点坐标, 还是从动点坐标
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}