2
\$\begingroup\$

I'm trying to make a cool simulation where a rope swings with an object attached to its end. I've set up the rope using Verlet integration and used Hooke's law to attach the object. But here's the catch: the object's movement is all over the place!

My simulation

I thought about just sticking the object to the end of the rope by setting its position to the rope last point's position, but this method won't account for the forces acting on the object, such as gravity.

Here's the code I'm using:

extends Node3D

class VerletRope:
    
    class Point:
        var locked: bool = false
        var pos: Vector3 = Vector3()
        var last_pos: Vector3 = Vector3()
        
        func _init(pos: Vector3, locked: bool = false):
            self.pos = pos
            self.last_pos = pos
            self.locked = locked
        
    class Stick:
        var point_a: Point
        var point_b: Point
        var length: float
        
        func _init(point_a: Point, point_b: Point, length: float):
            self.point_a = point_a
            self.point_b = point_b
            self.length = length
        
        
    var points: Array[Point] = []
    var sticks: Array[Stick] = []
    var gravity = 100.0
    var num_iterations = 30
    
    func _init(from: Vector3, to: Vector3, segment_distance: float, num_iterations: int = 30, gravity: float = 100.0) -> void:
        self.gravity = gravity
        self.num_iterations = num_iterations
        
        var rope_length = from.distance_to(to)
        var dir = from.direction_to(to)
        var segment_count = int(rope_length / segment_distance)
        var last_point = null
        
        for i in range(segment_count + 1):
            var pos = from + dir * (i * segment_distance)
            var point = Point.new(pos)
            self.points.append(point)
            
            if last_point:
                var length = last_point.pos.distance_to(point.pos)
                var stick = Stick.new(last_point, point, length)
                self.sticks.append(stick)
                
            last_point = point
        
    
    func simulate(delta: float) -> void:
        for point in points:
            if not point.locked:
                var last_pos = point.pos
                point.pos += point.pos - point.last_pos
                point.pos += Vector3.DOWN * gravity * delta * delta
                point.last_pos = point.pos
                
        for i in range(num_iterations):
            for stick in sticks:
                var center = (stick.point_a.pos + stick.point_b.pos) / 2.0
                var dir = (stick.point_a.pos - stick.point_b.pos).normalized()
                
                if not stick.point_a.locked:
                    stick.point_a.pos = center + dir * stick.length / 2.0
                    
                if not stick.point_b.locked:
                    stick.point_b.pos = center - dir * stick.length / 2.0
                    

@onready var ball = get_node("Ball")
@onready var verlet = VerletRope.new($Pole.position + Vector3(0, 2.0, 0), ball.position, 0.5)

var gravity = 9.81
var mass = 1.0
var velocity = Vector3()

var spheres = []

func _ready():
    verlet.points[0].locked = true
    verlet.points[-1].locked = true
    
    for point in verlet.points:
        var sphere = CSGSphere3D.new()
        sphere.radius = 0.1
        add_child(sphere)
        sphere.position = point.pos
        spheres.append(sphere)
            
        
func _physics_process(delta):
    verlet.points[-1].pos = ball.position
    verlet.simulate(delta)
    
    for i in range(verlet.points.size()):
        spheres[i].position = verlet.points[i].pos
    
    var net_force = Vector3()

    var tension = calculate_tension(ball.position, verlet.points[-2].pos)
    net_force.y -= mass * gravity
    net_force += tension


        
    var acceleration = net_force/ mass
    velocity += acceleration * delta
    ball.position += velocity * delta
    
    
func calculate_tension(start: Vector3, end: Vector3):
    var displacement = end - start
    
    var spring_constant = 100.0
    var force = displacement * spring_constant
    
    return force

You can check out the source along with the project files here too. My goal is to get that smooth, natural movement between the rope and the object, just like in the pictures.

Any tips or ideas would be appreciated, thanks in advance for the help!

Block and chain

Guillotine

Ball and rope

\$\endgroup\$
2
  • 1
    \$\begingroup\$ It looks like the motion of the mass at the end of the chain is having a tough time propagating up the chain. You might want to try adjusting the way you iterate the "sticks". Do you notice any difference if you iterate from the bottom up, or ping-pong between an upward and downward sweep? You might also want to consider solving constraints with two neighbouring points at once, which might help the energy transfer along. \$\endgroup\$
    – DMGregory
    Mar 18 at 11:37
  • 1
    \$\begingroup\$ Hey, thanks for the suggestions. I tried them out but couldn't quite get it to work with Verlet. I ended up ditching it and dealing only with spring forces. I might try to fix it later, but for now, that's good enough \$\endgroup\$ Mar 18 at 18:29

1 Answer 1

2
\$\begingroup\$

So, after some trial and error was able to reproduce something not that terrible!!! Ended up ditching verlet and dealing only with spring instead. Here's the result:

working spring rope

Here's the working source code if you need: https://github.com/SmiteIsTrashBro/RopePhysics/blob/master/spring_rope_test_3d.gd

extends Node3D


@onready var sphere_1 = $Sphere1
@onready var sphere_2 = $Sphere2

var spring_constant = 100.0
var gravity = 9.80
var damping_rate = 1.0
var velocity = Vector3()
var density = 1.0
    
    
class Point:
    var pos: Vector3 = Vector3()
    var force: Vector3 = Vector3()
    var velocity: Vector3 = Vector3()
    var rest_length: float = 0.0
    var mass: float = 0.0
    
    func _init(pos: Vector3):
        self.pos = pos
    
    
var points = []
var spheres = []

func _ready():
    var initial_pos = sphere_1.position
    var final_pos = sphere_2.position
    var segment_interval = 0.5
    var distance = initial_pos.distance_to(final_pos)
    var dir = initial_pos.direction_to(final_pos)
    var segment_count = ceil(distance / segment_interval)
    var corrected_interval = distance / segment_count
    var segment_mass = density * distance / segment_count
    
    for i in range(segment_count + 1):
        var pos = initial_pos + dir * (corrected_interval * i)
        var point = Point.new(pos)
        point.rest_length = corrected_interval
        point.mass = segment_mass
        points.append(point)
        
        var sphere = CSGSphere3D.new()
        add_child(sphere)
        sphere.radius = 0.05
        spheres.append(sphere)
    
    
func _physics_process(delta):
    if Input.is_action_pressed("ui_right"):
        points[0].pos.x += 5.0 * delta
    if Input.is_action_pressed("ui_left"):
        points[0].pos.x -= 5.0 * delta
    
    for i in range(1, points.size() - 1):
        var prev_point = points[i - 1]
        var point = points[i]
        var next_point = points[i + 1]

        var f_spring0 = calculate_spring_force(point, prev_point)
        var f_spring1 = calculate_spring_force(next_point, point)
        point.force = f_spring0 - f_spring1
        
        
    points[-1].force = calculate_spring_force(points[-1], points[-2])
        
    
    for i in range(1, points.size()):
        var point = points[i]
        var f_gravity = Vector3(0, -point.mass * gravity, 0)
        var f_damping = -point.velocity * damping_rate
        var acceleration = (point.force + f_gravity + f_damping) / point.mass 
        point.velocity += acceleration * delta
        point.pos += point.velocity * delta
        
    for i in range(points.size()):
        spheres[i].position = points[i].pos
        
    sphere_1.position = points[0].pos
    sphere_2.position = points[-1].pos
        

func calculate_spring_force(from: Point, to: Point):
    var displacement = from.pos - to.pos
    var x = displacement.length() - from.rest_length
    var f_spring = -spring_constant * x * displacement.normalized()

    return f_spring 

Some resources I stumbled uppon during my research:

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .