Rotating connected objects on correct axis

Godot Version

v4.4.1.stable.mono.official [49a5bc7b6]

Question

We have a game where we can pick up objects and stick them together to build bigger objects.

The locations on the objects that they can connect are defined by an Area3D componentized into a custom component called “SnapComponent”.

When they stick together we want the snap components to be aligned face to face with no rotation, but be able to have a “twist” rotation which is dependent on the angle that the connecting object was when connecting.

So if I were to stick two blocks together straight on it would look like this:

But if the red block was rotated slightly when attaching it would look like this:

The lines pointing out of the blocks are where the snap components are and the direction they face.

The following code is what we currently have that works to align the snaps face to face with no rotation:

Basis heldOriginalBasis = heldObject.GlobalTransform.Basis;
// Fetch the local transforms of connection areas relative to their respective bodies
Transform3D a1Local = tableObject.GlobalTransform.AffineInverse() * tableSnap.GlobalTransform;
Transform3D a2Local = heldObject.GlobalTransform.AffineInverse() * heldSnap.GlobalTransform;

Vector3 outward = a2Local.Basis.Z;

Transform3D a2Flipped = a2Local;
a2Flipped.Basis = a2Flipped.Basis.Rotated(outward, Mathf.Pi); // 180° around Y-axis
Transform3D alignment = a1Local * a2Flipped.AffineInverse();

heldObject.GlobalTransform = tableObject.GlobalTransform * alignment;
Transform3D newTransform = tableObject.GlobalTransform.AffineInverse() * heldObject.GlobalTransform;

Where heldObject and heldSnap are the object and snap component being placed on the object on the table which is tableObject and tableSnap.

“newTransform” then gets applied to the pieces of heldObject that get put on tableObject.

This code works but it does not apply any kind of “twist” so if I were to flip the red block upside-down before attaching, it would flip back upright when attaching again.

We have tried using the following code to achieve the twist rotation but it only works in certain cases:

/// <summary>
/// Given a rotational basis, snap to the nearest increment of the given degrees on all axes.
/// </summary>
private Basis SnapRotationToDegrees(Basis basis, float degrees) {
    // Convert basis to Euler angles
    Vector3 euler = basis.GetEuler();

    // Snap each axis to nearest degrees 
    float roundedAngle = Mathf.DegToRad(degrees);

    euler.X = Mathf.Round(euler.X / roundedAngle) * roundedAngle;
    euler.Y = Mathf.Round(euler.Y / roundedAngle) * roundedAngle;
    euler.Z = Mathf.Round(euler.Z / roundedAngle) * roundedAngle;

    // Fix floating point issues
    float limit = 1e-6f;
    if (Mathf.Abs(euler.X) < limit) euler.X = 0.0f;
    if (Mathf.Abs(euler.Y) < limit) euler.Y = 0.0f;
    if (Mathf.Abs(euler.Z) < limit) euler.Z = 0.0f;

    // Convert back to basis and return conversion
    return Basis.FromEuler(euler);
}

and then this after the first block of code I shared

Basis snappedBasis = SnapRotationToDegrees(heldOriginalBasis, 30);
newTransform = new Transform3D(snappedBasis, newTransform.Origin);

This code works when the snap component is at certain angles. For example if I were to rotate the red block as I had for the second image but then placed it on the top of the blue block it rotates it like this:

I believe this is due to the axis in which it rotates does not change depending on the side of the block it is connected.

My initial thought was that the line Vector3 outward = a2Local.Basis.Z; needed to be dependent on the side of the block but when changing this to anything other than Z it has even worse effects. Changing it to X makes the red block connect inside the blue block. And changing it to Y makes it’s rotation off in more than one axis weirdly.

If anyone has worked with this kind of rotational calculations before and has an idea on how to do this any help is welcome. If more context is needed please ask.

My initial impression is you are way overcomplicating this.

What kind of nodes are the blocks? What’s the node tree look like?

You should be able to use collisions to make sure the blocks can’t intersect, and to detect which side is touching. Then position is handled by the physics engine. Then you literally just determine which rotation you want to copy, and copy it. Like if you want the same rotation on the x-axis:

box_a.rotation.x = box_b.rotation.x
1 Like

There’s a lot of reasons we require it to work like this that’s just due to how the game needs to work.

All the objects are rigidbody3Ds and they all have a node called connectionComponent which contains all the snap components.

We’ve done it like this in order to be able to define how the objects can attach to each other.

I understand that this seems more complicated than necessary but this is the way we need to do it.

We are just struggling to apply the maths in order to convert that rotation into the final transform

Your problems sound similar to Gimbal Lock. I’d recommend you look into Quaternion rotation.

I’ll have a look at that thank you for your help

1 Like

Having looked into Quaternions I’m finding translating the concept behind it to how I would actually implement it very challenging. I am finding it hard to picture what functions I would need to use in order to achieve the rotation I am looking for.

Just for the sake of being thorough here is some more context.


This is the scene tree for one of the cubes.
ConnectionComponent is the node that contains all the connection code and is where I am implementing this rotation.
Each node below that is an custom node extending from Area3D and their respective collision shapes.

Each Area3D is rotated to face outward in the direction that it needs to connect to other areas



And importantly, the collision shape has no rotation or position changes. Everything is 0, 0, 0.

What I need to do is get the axis on the object that when rotated, rotates perpendicular to the Area3D that is being connected. (This isn’t necessarily always going to be on a flat face pointing out of the centre of the object). And using that axis to rotate the object when connecting by the objects global rotation in that axis before connecting.

3D rotations, my favorite mind twister :slight_smile:
Putting these 2 statements together and looking at your last cube screenshot

We can assume that the global axis you want to rotate your connecting block would be:

var rotation_axis_global: Vector3 = connecting_area3d.global_basis * Vector3.FORWARD

Then you should be able to rotate your other cube around this global axis:

var angle: float = PI / 4
global_rotate(rotation_axis_global, angle)

Here’s my little demo that I did with 2 cubes. The yellow cube I’m controlling directly and just change its rotation by 90 degrees with arrow keys. The red cube rotates by itself by the axis that the blue raycast of the yellow cube is pointing at. The raycasts don’t do anything by themselves, these just helps with visualization. I’m also resetting the red cube’s rotation after every change of the axis, so that it doesn’t end with weird angles, cause it’s harder to understand which way it rotates.

This is the scene tree:

This is the code on the yellow cube:

extends MeshInstance3D

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_cancel"):
		get_tree().quit()

func _process(_delta: float) -> void:
	var direction: Vector2 = Vector2.ZERO
	if Input.is_action_just_pressed("ui_left"):
		direction += Vector2.LEFT
	elif Input.is_action_just_pressed("ui_right"):
		direction += Vector2.RIGHT
	elif Input.is_action_just_pressed("ui_down"):
		direction += Vector2.DOWN
	elif Input.is_action_just_pressed("ui_up"):
		direction += Vector2.UP
	rotate((global_basis * Vector3.DOWN).normalized(), direction.x * PI / 2)
	rotate((global_basis * Vector3.RIGHT).normalized(), direction.y * PI / 2)

This is the code on the red cube:

extends MeshInstance3D

@export var rotator_cube: Node3D
var old_axis: Vector3

func _process(delta: float) -> void:
	var rotation_axis_global: Vector3 = rotator_cube.global_basis * Vector3.FORWARD
	
	if rotation_axis_global != old_axis:
		global_rotation = Vector3.ZERO
		old_axis = rotation_axis_global
	
	global_rotate(rotation_axis_global, delta * 3)

Let me know if that helps you, or there’s any more explanation needed.

1 Like