I’m trying to get my player to pick up this shovel and have it rotate so that it’s properly aligned with both the player’s hands. Here’s what I’m working with:
The shovel (with a sphere mesh where I want the hand to grip)
The function that is called by Player when Shovel is picked up:
func pick_up(object: HoldableObject) -> void:
anim.hold(object.hands) # Sets the position of the arms to hold object
object.reparent(hand_r_bone)
object.rotation_degrees = Vector3.ZERO
object.position = -(object.g1.position * object.scale)
With that function, I manage to get the shovel positioned in the right hand the way I want it. Now the challenge is getting it to rotate so that it’s also placed in the left hand.
Directly construct shovel’s global basis. Its y vector is known; it is the direction from character’s left hand to their right hand. You’d typically want basis x vector to be perpendicular to global up and to already known basis y. So it can be calculated as a cross product of those two vectors. Since this basis is orthogonal, the remaining z vector is simply a cross product of x and y vectors you just calculated.
This is basically an equivalent of a custom look_at() function. You need to use custom instead of built-in look_at() because the latter aims object’s z axis and you need to aim the y axis.
Thanks for the quick reply! I’m doing my best to apply what you’re describing but I’m not quite getting it as I’m pretty new to Transform3D stuff (I just learned what a basis is today and still trying to wrap my head around it). Would you mind explaining it more simply?
From what Normalized said i imagine you could also rotate the shovel mesh in the shovel scene to be aligned in the positive z direction, then give it the appropriate twist so that the y is correct too.
Then when you position the shovel in the world you just rotate it until it looks the same.
Hmm I suppose that could work, but the reason I want to do the rotation via script after the item is picked up is so that I can use the logic in a function that is called in _process() and have it always align with the hands, even when an animation plays (e.g. attack or dig animation)
For the sake of this discussion, the basis is a collection of 3 mutually perpendicular vectors. You can see them on your screenshot drawn as red, green and blue arrows. Directions of those 3 vectors unambiguously determine orientation of an object in 3D space. In other words, the basis represents the orientation of object’s local coordinate system. When you rotate an object using the rotation gizmo in the editor, you’re in fact re-orienting the basis, those 3 red/green/blue arrows.
So if you calculate desired directions of those 3 vectors and just directly assign them to global_basis.x, global_basis.y and global_basis.z - the object will magically pop into wanted orientation.
One of those vectors is immediately known - the green one aka basis y. It’s the direction from one hand to another. So that can be immediately assigned to global_basis.y
The remaining two must be calculated. The neat thing here is that those 3 vectors all need to be mutually perpendicular. We can exploit the vector cross product here as the cross product between any two vectors is always perpendicular to both of them.
Next we calculate the basis x. This, by our previous definition, needs to be perpendicular to our known basis y. We also want it horizontal so it needs to be perpendicular to global up vector as well. If we take the cross product of basis y and global up vector - we get our basis x.
Now we have two basis vectors; the green one (basis y) and the red one (basis x). Since the remaining blue one must be perpendicular to both of those, we simply take their (x and y) cross product and that’s our blue basis z.
The cross product doesn’t “preserve” lengths of the vectors so the final step is to normalize them i.e. make their length exactly 1. The Basis object has a function "orthonormalized()` that normalizes all 3 vectors at once. So calling that should be the last step in constructing the basis.
Hmm, i used to do a trick in GL/D3D … something like rotate the shovel in script relative to the root node, then take the matrix from the look at function and apply the transform to the root then unrotate the shovel.
It might make more sense to write a custom look at.
Awesome explanation. I haven’t managed to get it quite right yet but this really helped me better understand the concepts. I’m going to take a break to rest my brain a bit and then try again and hopefully report back with a working function.
Problem with shovels and objectslike that it that they often spin about the handle and thats not central to the object. The object origin kind of sets up another ‘translate’ SO3 matrix operation.
I managed to get the shovel rotated and positioned how I wanted! Thanks again for your detailed explanation
func pick_up(object: HoldableObject) -> void:
anim.hold(object.hands) # Position hands
object.reparent(hand_r_bone)
# Calculate Y basis from hand positions
object.global_basis.y = hand_l_bone.global_position.direction_to(hand_r_bone.global_position).normalized()
# Calculate X basis (perpendicular to basis.y and global Y axis)
object.global_basis.x = object.global_basis.y.cross(Vector3.UP)
# Calculate Z basis (perpendicular to X and Y basis)
object.global_basis.z = object.global_basis.x.cross(object.global_basis.y)
# Move to hand position
object.position = Vector3.ZERO
object.translate_object_local(-object.grip.position)
Don’t forget to normalize the whole basis in the end. The length of cross product of non perpendicular unit vectors will not be 1.0. Since lengths of basis vectors determine object scaling you may get unwanted scaling artifacts.
So when constructing the basis in this way always finish with normalization:
Also note that if the object has any scaling other than (1,1,1) you’ll need to additionally take care of that if you want to preserve the initial scaling, because, as already mentioned, the basis vectors encode scaling as well.
What if the bone attachment had a child node that automatically looks at the left hand, then that node had another child node oriented perpedicular to be in wieldable compatible coordinates ? You could just reparent the shovel to get the same effect.
I had a similar problem with a flame sprite … the flame sprite was a crossed pair that needed to point upwards, but the sprite was facing z, so lookat failed. I ended up using a y axis billboard with an offset. Now the flame doesnt even show up on mobile … just a cyan rectangle ….
Yeah that sounds plausible. I could also add a bone to the model and animations themselves in Blender to use as an anchor point.
I went with the scripted Transform3D approach for the purpose of this question as I wanted to learn more about basis and gdscript in general. Thanks for the suggestions though!
The simplest way without using any math code is to re-orient the vertex data in a mesh editing app so the shovel points up the local z axis. Then just call look_at() or parent it to a look at node.
Appreciated that the scripted method is useful as theres a lot of cases where the node orientations are difficult to work with. A classic example is when you want (rotating) springs connecting the nodes - the sibling relation could be more appropriate than hierachical in that case, like for example if you wanted the following node to spring to the parent orientation.
If the shovel had to leave one hand or got knocked to the side with recoil its good to control the node with script or physics and the parent transform would get in the way. Blender has a constraint called CopyTransform … if you wanted custom motion on the node you could also write a copy transform script that then switches mode easily.
You could also probably use look at on an empty node, get the basis then swizzle the vectors … (x,y,z) →(z,x,y) … or whatever is needed to orient the shovel, then copy the new basis to a parent node that just pre rotates the shovel.
Edit: after messing about with swizzles and basis copying ive confirmed the technique should work …
The copy/swizzle node is the child of root, the source node is just floating freely.
extends Node3D
@export var axis : int = 0
@export var b_still : bool = false
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
if b_still:
return
if axis == 0:
rotate_x(0.1*delta)
elif axis == 1:
rotate_y(0.1*delta)
elif axis == 2:
rotate_z(0.1*delta)
and the copying node:
extends Node3D
@export var copy_transform : Node3D
@export var swizzle_transform:bool = false
@export var swizzle_num :int = 0
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
if not swizzle_transform:
var new_basis = copy_transform.global_basis
self.basis = new_basis
else:
if swizzle_num == 0:
var new_basis = copy_transform.global_basis
self.basis.x = new_basis.z
self.basis.y = new_basis.x
self.basis.z = new_basis.y
elif swizzle_num == 1:
var new_basis = copy_transform.global_basis
self.basis.x = new_basis.y
self.basis.y = new_basis.z
self.basis.z = new_basis.x
You could also swizzle like (xyz)→(-y,x,z) but I didn’t bother this time.