It's not hard to create a nice static physical rope using blender. However in some scenarios we have to create physical ropes at runtime. This article tries to solve the problem below:
PhysicsBody3Dand how long the rope is(the length is always longer than the distance between two bodies), connect them at runtime with a rope that reacts to physics system.
I solved this problem in an abandoned 3D space werewolf game project last year. Now I want to share with you how I do it.
Physical Rope Basics
Similar to fluid matter simulation, a continuous physical rope is often divided into discrete parts.
Each circle is a JointPin3D. Segment(rectangles in the image) will not be drawn as it is only actually abstract concept here(We will talk about the rendering in part 2). Each joint connects two neighbour joint or physical body. By using Joints, we get a physical rope of 6 degrees of freedom.
Tips: To make Joint work, we need at least one
In order to create the rope at runtime, we accept 4 inputs.
resolution: int, segments count. The higher this value, the more continuous this discrete simulation will feel like.
rope_length: float, how long the rope is. Of course this must be bigger than the distance between two anchors.
start_point: Node3D, a Node3D providing the information of global position of the starting anchor of the rope whose parent node is the physical body.
end_point: Node3D, same as above but provide the information of the ending anchor.
Basically we just need two steps:
- Place segments correctly along the rope, we can easily calculate segment length from
resolutionwhich is joints count.
- In each gap between every two segments, we put a joint right at it, connecting two neighbour segments.
Step 2 is fairly easy, we need to talk more on step 1.
Arrange the Positions of Segments
How to know the positions of segments even before we create them?
Well, we need to draw a line between two anchors, then arrange segments along this line evenly given
segment_length can be very small like
0.1 (Pinjoint3D will work the same whatever the initial distance is),
joint_length = (rope_length - resolution * segment_length) / joint_count,
joint_count = resolution + 1.
We place first and last joint at the position of start point and end point to let module user to customize.
As for situations where
resolution is 1 or 2, the problem is much simpler.
resolution is 1, there is only one segment connecting two joints.
resolution is 2, joints count is 3, of which 2 are at anchors positions and segments count is 2(same as
resolution). To make these 3 joints positioned evenly, just place the left one in the middle.
resolution is greater than 2, things get interesting. As the rope length is longer than the distance, we must bend the line into two parts at a turning point. Let's say point T, and let's assume we need to connect body A and body B. The line is divided into two parts, one part is from body A to point T and the other one is from point T to body B. Then we just need to place pinjoints evenly along these two parts.
Now we need to calculate the global position of point T.
Suppose the length of part before T is
r1, the length of part after T is
r2. It is not hard to find that the trajectory of T is a circle, intersecting by two spheres whose radiuses are
We already know:
- two centers of the spheres, let's call them
- the normal of the intersecting circle, which is a vector between
r2, we can approximately consider they are nearly half of the
r1does not equal
r2because of the parity of
resolution, that's totally ok.
With parametric equations, we can solve this spatial circle.
$ x (\theta) = c_1 + r cos ( \theta ) a_1 + r sin ( \theta ) b_1 $
$ y (\theta) = c_2 + r cos ( \theta ) a_2 + r sin ( \theta ) b_2 $
$ z (\theta) = c_3 + r cos ( \theta ) a_3 + r sin ( \theta ) b_3 $
where $(c_1, c_2, c_3 $) is the center of the circle, $\vec a$ and $\vec b$ are perpendicular unit vectors inside the circle, they are perpendicular to each other and perpendicular to the normal $\vec n$.
Here is the circle code:
class_name SphereSphereCircle extends Object var _rng = RandomNumberGenerator.new() # spheres var _sphere_a_center: Vector3 var _sphere_a_radius: float var _sphere_b_center: Vector3 var _sphere_b_radius: float var _sphere_sphere_distance: float # triangle var cosA: float var a: float var b: float var c: float # circle var center: Vector3 var radius: float var vector_a: Vector3 var vector_b: Vector3 var normal: Vector3 func _init(sphere_a_position: Node3D, sphere_a_radius: float, \ sphere_b_position: Node3D, sphere_b_radius: float): _sphere_a_center = sphere_a_position.global_transform.origin _sphere_a_radius = sphere_a_radius _sphere_b_center = sphere_b_position.global_transform.origin _sphere_b_radius = sphere_b_radius _sphere_sphere_distance = _sphere_a_center.distance_to(_sphere_b_center) _calc_center() _calc_radius() _calc_vectors() func _calc_center(): a = _sphere_b_radius b = _sphere_a_radius c = _sphere_sphere_distance cosA = (pow(b, 2) + pow(c, 2) - pow(a, 2)) / (2 * b * c) var k := b * cosA / c normal = _sphere_b_center - _sphere_a_center center = _sphere_a_center + k * normal func _calc_radius(): var sinA = sqrt(1 - pow(cosA, 2)) radius = b * sinA func _calc_vectors(): var unit = Vector3.UP if not unit.cross(normal) == Vector3.ZERO: vector_a = unit.cross(normal).normalized() else: unit = Vector3.RIGHT vector_a = unit.cross(normal).normalized() vector_b = normal.cross(vector_a).normalized() func get_point(theta: float) -> Vector3: var point := center + radius * cos(theta) * vector_a + radius * sin(theta) * vector_b return point
Initialize the circle, just randomize a
theta between $ [0, 2 \pi] $, and call
get_point(theta), you get the global position of point T.
As we have point T, the rest of the work is just placing segments evenly alongside the bended line and connecting segments or phyiscal bodies with
Now we have
segments consisted of
RopeSegment, each of them has correct global position. Now what we do is to connect them with
func _add_joints(): var first_segment: RopeSegment = segments var last_segment: RopeSegment = segments[resolution - 1] var first_joint = _generate_joint_between(head_anchor, first_segment) var last_joint = _generate_joint_between(last_segment, tail_anchor, 0) first_segment.add_child(first_joint) first_joint.global_transform.origin = start_point.global_transform.origin last_segment.add_child(last_joint) last_joint.global_transform.origin = end_point.global_transform.origin for i in len(segments): if segments.size() >= i + 2: var segment: RopeSegment = segments[i] var next_segment: RopeSegment = segments[i + 1] var joint = _generate_joint_between(segment, next_segment) segment.add_child(joint) var sphere_middle_point = segment.global_transform.origin + (next_segment.global_transform.origin - segment.global_transform.origin) * 0.5 joint.global_transform.origin = sphere_middle_point
_generate_joint_between() just setup the constraints for game engine's physics to resolve.
func _generate_joint_between(body_a: PhysicsBody3D, body_b: PhysicsBody3D, priority: int = 1) -> PinJoint3D: var joint := PinJoint3D.new() joint.set_node_a(body_a.get_path()) joint.set_node_b(body_b.get_path()) joint.set_solver_priority(priority) return joint
For more detail, you can checkout the github repo.
In next article, we will talk about the rendering part.