Move and rotate object around pivot and resume from where it stopped
Asked Answered
B

2

2

I'd like to move an object around another - just as if the one object was a child of the other. This is GDscript - Godot Engine 3.2, but the logic should be very similar to other game engines.

Whenever I hold spacebar the green cube follows the blue cubes rotation.

First GIF demonstrates the green cube starting at position Vector3(0, 4, 0) without any rotation applied. This works perfectly fine.

In the second GIF I'm holding and releasing spacebar repeatedly. I would expect the green cube to continue from where it left, but instead it "jumps" to a new position and continues from there.

enter image description here

enter image description here

Code below does not include the actual rotation of the blue cube (pivot point), but only the calculations needed to move/rotate the green cube. Rotation of the blue cube is not an issue. Also, rotation is just for the sake of demonstration - in real scenario the blue cube would be moving around as well.

Rotations are calculated using quaternion, but this is not a requirement.

extends Node

var _parent
var _child
var _subject
var _positionOffset: Vector3
var _rotationOffset: Quat

func _ready():
    _parent = get_parent()
    _child = $"/root/Main/Child"

func _input(event):
    if event is InputEventKey:
        if event.scancode == KEY_SPACE:
            if event.is_action_pressed("ui_accept") and  _child != null:
                _subject = _child
                _set_subject_offset(_parent.transform, _child.transform)
            elif event.is_action_released("ui_accept"):
                _subject = null

func _set_subject_offset(pivot: Transform, subject: Transform):
    _positionOffset = (pivot.origin - subject.origin) 
    _rotationOffset = pivot.basis.get_rotation_quat().inverse() * subject.basis.get_rotation_quat()

func _rotate_around_pivot(subject_position: Vector3, pivot: Vector3, subject_rotation: Quat):
    return pivot + (subject_rotation * (subject_position - pivot))

func _physics_process(_delta):
    if _subject == null: return

    var target_position = _parent.transform.origin - _positionOffset
    var target_rotation = _parent.transform.basis.get_rotation_quat() * _rotationOffset
    _subject.transform.origin = _rotate_around_pivot(target_position, _parent.transform.origin, target_rotation) 
    _subject.set_rotation(target_rotation.get_euler())

I feel like I'm missing something obvious.

Boyes answered 27/1, 2020 at 19:58 Comment(0)
E
1

You can easily achieve this by temporarily parenting the subject to the pivot point while preserving the global transform:

extends Spatial

onready var obj1: Spatial = $Object1
onready var obj2: Spatial = $Object2

func reparent(obj: Spatial, new_parent: Spatial):
    # Preserve the global transform while reparenting
    var old_trans := obj.global_transform
    obj.get_parent().remove_child(obj)
    new_parent.add_child(obj)
    obj.global_transform = old_trans

func _physics_process(delta: float):
    obj1.rotate_z(delta)

func _input(event: InputEvent):
    if event.is_action_pressed("ui_accept"):
        reparent(obj2, obj1)
    elif event.is_action_released("ui_accept"):
        reparent(obj2, self)

enter image description here

If reparenting isn't feasible, you could instead parent a RemoteTransform to the pivot, and have that push its transform to the object you want to rotate:

extends Spatial

onready var remote_trans: RemoteTransform = $RemoteTransform

func _process(delta):
    rotate_z(delta)

func attach(n: Node):
    # move the remote so the target maintains its transform
    remote_trans.global_transform = n.global_transform
    remote_trans.remote_path = n.get_path()

func detach():
    remote_trans.remote_path = ""
Epiphany answered 27/1, 2020 at 21:22 Comment(2)
Thanks, I'll probably use this for now. The downside with this solutions is the the node tree needs to be structured in a certain way otherwise the game might break. Also, I cannot easily hide the parent pivot/parent node, without also hiding the child node. Of course there are workarounds this - and those workarounds might be easier than figuring out the math :)Boyes
@Boyes fair point. I've added a second solution that doesn't require reparenting (or math :D)Epiphany
E
1

repost, Thank You path9263, answered Jun 4, 2019 by path9263 (134 points)

func rotate_around(obj:Spatial, point:Vector3, axis:Vector3, phi:float):
    # https://godotengine.org/qa/45609/how-do-you-rotate-spatial-node-around-axis-given-point-space?show=45970#a45970
    obj.global_translate(-point)
    obj.transform = obj.transform.rotated(axis, phi)
    obj.global_translate(point)

the below functions do not work

func rotate_around_point(transform_me:Spatial, pivot_point:Vector3, axis:Vector3, phi:float):
    # #4: busted, no rotation
    # https://mcmap.net/q/21930/-move-and-rotate-object-around-pivot-and-resume-from-where-it-stopped
    # Preserve the global transform while reparenting
    var pivot_spatial = Spatial.new()
    pivot_spatial.translation = pivot_point
    var parent_orig = transform_me.get_parent()
    var transform_orig = transform_me.global_transform
    parent_orig.remove_child(transform_me)
    pivot_spatial.add_child(transform_me)
    transform_me.global_transform = transform_orig
    
    pivot_spatial.rotate(axis, phi)
    
    var transform_rotated = transform_me.global_transform
    pivot_spatial.remove_child(transform_me)
    parent_orig.add_child(transform_me)
    transform_me.global_transform = transform_orig
func rotate_around_point(transform_me:Spatial, pivot_point:Vector3, axis:Vector3, phi:float):
    # busted, no rotate
    # https://mcmap.net/q/21930/-move-and-rotate-object-around-pivot-and-resume-from-where-it-stopped
    var remote_trans : RemoteTransform = RemoteTransform.new()
    remote_trans.global_transform = transform_me.global_transform
    remote_trans.remote_path = transform_me.get_path()
    remote_trans.rotate(axis, phi)
    remote_trans.remote_path = ""

func rotate_around_point(transform_me:Spatial, pivot_point:Vector3, axis:Vector3, phi:float):
    # busted, local rotate
    # https://godotforums.org/discussion/comment/43335/#Comment_43335
    var gto_orig = transform_me.global_transform.origin
    transform_me.global_transform.origin = pivot_point
    transform_me.rotate(axis, phi)
    transform_me.global_transform.origin = gto_orig

func rotate_around_point(transform_me:Spatial, pivot_point:Vector3, axis:Vector3, phi:float):
    # busted, no rotate
    # https://godotengine.org/qa/34248/rotate-around-a-fixed-point-in-3d-space?show=38928#a38928
    var start_position : Vector3 = transform_me.translation
    var pivot_radius : Vector3 = start_position - pivot_point
    var pivot_transform : Transform = Transform(transform.basis, pivot_point)
    var transform = pivot_transform.rotated(axis, phi).translated(pivot_radius)
    transform.xform(transform_me)
Everett answered 27/1, 2020 at 19:58 Comment(0)
E
1

You can easily achieve this by temporarily parenting the subject to the pivot point while preserving the global transform:

extends Spatial

onready var obj1: Spatial = $Object1
onready var obj2: Spatial = $Object2

func reparent(obj: Spatial, new_parent: Spatial):
    # Preserve the global transform while reparenting
    var old_trans := obj.global_transform
    obj.get_parent().remove_child(obj)
    new_parent.add_child(obj)
    obj.global_transform = old_trans

func _physics_process(delta: float):
    obj1.rotate_z(delta)

func _input(event: InputEvent):
    if event.is_action_pressed("ui_accept"):
        reparent(obj2, obj1)
    elif event.is_action_released("ui_accept"):
        reparent(obj2, self)

enter image description here

If reparenting isn't feasible, you could instead parent a RemoteTransform to the pivot, and have that push its transform to the object you want to rotate:

extends Spatial

onready var remote_trans: RemoteTransform = $RemoteTransform

func _process(delta):
    rotate_z(delta)

func attach(n: Node):
    # move the remote so the target maintains its transform
    remote_trans.global_transform = n.global_transform
    remote_trans.remote_path = n.get_path()

func detach():
    remote_trans.remote_path = ""
Epiphany answered 27/1, 2020 at 21:22 Comment(2)
Thanks, I'll probably use this for now. The downside with this solutions is the the node tree needs to be structured in a certain way otherwise the game might break. Also, I cannot easily hide the parent pivot/parent node, without also hiding the child node. Of course there are workarounds this - and those workarounds might be easier than figuring out the math :)Boyes
@Boyes fair point. I've added a second solution that doesn't require reparenting (or math :D)Epiphany

© 2022 - 2024 — McMap. All rights reserved.