用Unity实现简单的绳子模拟(一)

本文介绍了如何使用Unity的API实现一个基于Mass-Spring模型的简单绳子模拟。通过胡克定律计算弹簧力,利用时间积分推算粒子运动。在实现过程中,探讨了Explicit Euler时间积分方法的局限性,以及Damping和Air Resistance参数对模拟效果的影响。文章提供了Github链接供读者进一步研究。
部署运行你感兴趣的模型镜像

用Unity实现简单的绳子模拟(一)

说到Unity物理,一般都会想到内置的physX物理引擎。其实我们也可以用Unity的API去实现一些简单的物理算法。
本文会介绍如何从头实现一个简单的绳子模拟的小Demo。
Mass-Spring绳子Demo

物理模型

物理模拟的第一步,就是要对真实世界的事物做简化,转化成可计算的模型。对于绳子,我们最直观的简化方法就是把它想象成一堆由弹簧连接的小球(一般也叫粒子)。这就是Mass-Spring模型。
绳子的Mass-Spring模型
用胡克定律我们就可以简单的求出弹簧力。
F = k * (L - L0)
其中k是弹簧系数,L是当前弹簧的长度,L0是弹簧不受力时的长度(初始长度)。

那么每个粒子的受力就很容易算出来了。
在这里插入图片描述

时间积分

有了粒子受到的外力之后,就需要开始考虑粒子如何跟着受力在时间的维度里运动了。
物理模拟的时间间隔通常是恒定的,我们这里就把时间间隔记为dt。
通常,只有初始时刻的参数是已知的,我们会根据初始时刻的参数(位置,速度,加速度)来推断下一时刻的位置。
在这里插入图片描述
图中的p代表粒子位置,v代表速度,a代表加速度,F代表受到的合力。
结合高中物理知识,上面的图应该比较容易理解。实际上,我们是在时间维度上做积分。这里我们简化地认为在dt足够小的时候,这段时间的运动可以认为是匀速运动。上图中的积分模式叫做Explicit Euler,这是一种误差大且不太稳定的时间积分方法,有空会仔细讨论不同的时间积分方法以及他们的误差分析。

实现

OK,现在就开始实现这样一个简单的绳子模拟器吧。

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

namespace PhysicsLab
{
    public class RopeSpringSolver : MonoBehaviour
    {
        public Transform ParticlePrefab;
        public int SubStepCount = 10;
        public int Count = 3;
        public int Space = 1;
        public float SpringK = 1.0f;
        public float AirResistanceRatio = 0.1f;
        [Range(0, 1)]
        public float Damping = 0.1f;

        public Vector3 ExternForce = Vector3.zero;

        private List<Transform> chain = new List<Transform>();
        private List<SpringParticle> particleList = new List<SpringParticle>();

        void Start()
        {
            for (int i = 0; i < Count; i++)
            {
                var obj = Instantiate(ParticlePrefab, transform, true);
                obj.Translate(0, -i * Space, 0);
                chain.Add(obj);

                // Construct Particles
                var particle = new SpringParticle();
                particle.invMass = 1;
                particle.radius = 0.5f * Space;
                particle.pos = new Vector3(0, -i * Space, 0);
                particle.velocity = Vector3.zero;
                particleList.Add(particle);
            }
        }

        void FixedUpdate()
        {
            float dt = Time.fixedDeltaTime / SubStepCount;

            // Update Particle Position
            // Root Particle follow Transform
            particleList[0].pos = transform.position;
            for (int n = 0; n<SubStepCount; n++)
            {
                for (int i = 1; i < Count; i++)
                {
                    var particle = particleList[i];

                    // Calculate Spring Force
                    // Last Particle
                    Vector3 forceDir = particleList[i - 1].pos - particle.pos;
                    Vector3 springForce = SpringK * forceDir.normalized * (forceDir.magnitude - Space);

                    // Next Particle
                    if (i < Count - 1)
                    {
                        forceDir = particleList[i + 1].pos - particle.pos;
                        springForce += SpringK * forceDir.normalized * (forceDir.magnitude - Space);
                    }

                    // Update Particle Position according to Newton's 2nd Law
                    particle.pos += (1 - Damping) * particle.velocity * dt;

                    // Update velocity
                    Vector3 acceleration = (springForce + ExternForce - AirResistanceRatio * particle.velocity.magnitude * particle.velocity) * particle.invMass;
                    particle.velocity += acceleration * dt;
                }
            }

            // Apply Particle Position to Transform
            for (int i=0; i<Count; i++)
            {
                chain[i].position = particleList[i].pos;
            }
        }

        void OnDrawGizmos()
        {
            if (particleList == null || particleList.Count != Count) return;

            Gizmos.color = Color.blue;
            for (int i = 1; i < Count; i++)
            {
                var particleParent = particleList[i - 1];
                var particle = particleList[i];
                Debug.DrawLine(particleParent.pos, particle.pos);
            }
        }
    }

    public class SpringParticle
    {
        public float invMass; // 1 / mass
        public float radius;
        public Vector3 pos;
        public Vector3 velocity;
    }
}

Github地址:https://github.com/ossupero/UnityStrandSimulator/tree/master/Assets/Rope/SpringRope

讨论

如果亲自试验一把的话,相信很容易发现问题——绳子的移动好像非常缓慢。
这是因为默认设置把DampingAir Resistance调得比较高。这两个参数的主要用途是在增加阻力,让速度不要变得太大。
那么,如果把这两个值调到0会怎么样?
数值不稳定的示例
绳子开始抽风,然后数值爆炸,出现NaN的错误提示。这就是Explicit Euler的不稳定性导致的结果。
所以弹簧模型在实际应用的时候,通常会用更稳定的Implicit Euler来做时间积分,但是这种积分方法实现更复杂,效率也更差。
另一种目前比较流行的物理建模方式是Position Based Dynamics,实现简单,效率高,会比这里的弹簧稳定很多:)

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值