Apply_torque on two axes, going insane :(

Godot Version

4.3 stable

Question

Hi all,

In a nutshell, I want to be able to apply a torque on one axis without interfering with a torque on another axis on the same object.

Picture a wheel lying on the ground. Flat circle face to the ground. When the player (the wheel) moves forward, the wheel should raise upright (rotation on z) and start rolling (rotation on x) forward.

wheel

I originally used plain rotations until I understood they were a big no no with anything physically simulated. After watching this video, I was mind blown and decided that a PID controller was the right way to make the wheel stay upright in my game. This part actually works pretty well. But when I start rolling forward, either nothing happens or if it does, the PID goes crazy and cranks the torque on Z so hard that the wheel slaps the ground and flies to the sky.

The wheel is a RigidBody3D. I am updating it with apply_torque in in _physics_process instead of _integrate_forces as I need a delta value to update the PID function that corrects the Z torque. This being said, I have tried in _integrate_forces with a hard coded delta and the issue is the same.

I have been trying to implement this for 2 weeks now. I have watched and read everything about transforms and coordinate systems and I don’t see the rule that I’m breaking. Why is a rotation on X interfering with the value of my rotation on Z?

Here is the naughty code:

func _physics_process(delta: float) -> void:
	# Put the camera near the object
	$SpringArmPivot.global_transform.origin = global_transform.origin
	
	# The PID controller tries to keep the rotation at 90 degrees to keep the wheel upright 
	var pid
	pid = _pid1D.update(rotation_degrees.z - TARGET_ANGLE, delta) * 0.03
	
	# Get direction input from player
	inputDirection = Input.get_vector("move_l", "move_r", "move_f", "move_b")
	# This one will be used to roll in the direction the camera is looking at
	globalDirection = ($SpringArmPivot.transform.basis * Vector3(inputDirection.x, 0, inputDirection.y)).normalized()
	
	if inputDirection:
		# Lock the y axis to avoid jittering
		axis_lock_angular_y = true
		# If the wheel rotation is below or above 90 degrees we apply a torque to keep it upright
		if abs(rotation_degrees.z) < TARGET_ANGLE:
			apply_torque(Vector3(0, 0, 1) * -pid)
		elif abs(rotation_degrees.z) > TARGET_ANGLE:
			apply_torque(Vector3(0, 0, 1) * pid)
		# If we're upright we roll forward
		else:
			if inputDirection.y == -1 :
				apply_torque(Vector3(1, 0, 0))
	# Just for debugging:
	$CanvasLayer/Label.text = "%s %s" % [rotation_degrees.z, pid]

Thank you for your time

EDIT: Added gif, clarifications and comments on the code

Bumping because I added details to the question. Hope that’s ok, couldn’t find any rules against bumping in the guidelines :slight_smile:

Since the PID controller is what drives the torque amount, I think it would be a good idea to include the code for it.

Your code uses the wheel’s local rotation in both the PID controller and the conditional logic. This is unlike apply_torque(), and similar methods, which expect values to be in global space. Therefore…

Q1: Is there a specific reason you’re using the local space (not the global space)?

Q2: What is the 0.03 that is multiplied with the PID output; what does it achieve?

The conditions:

  • abs(rotation_degrees.z) < TARGET_ANGLE
  • abs(rotation_degrees.z) > TARGET_ANGLE

…cover all possible outcomes except for when abs(rotation_degrees.z) == TARGET_ANGLE. This means that you will only start rolling forward when the rotation is exactly on the TARGET_ANGLE boundary. Unless there is something I’m missing here, that part of your code looks wrong.


I’m still not sure about the PID part of your code so it will be hard for me to be confident in what I’m saying here. Hopefully you can provide more info on that part. I’ll be happy to assist you until a solution is found.

1 Like

Cheers, @Sweatix!

The code for the PID is at the bottom of this post, with a reference to the original code, well, video. I understand most of it apart from maybe, why it features extends RefCounted.

Your code uses the wheel’s local rotation in both the PID controller and the conditional logic. This is unlike apply_torque(), and similar methods, which expect values to be in global space.

There probably lies the point that I’m not getting. I feel like I need to use local space to rotate around the axes (or axises of you’re in the US :slight_smile:) of the object. I thought that if I used global space my center of rotation would be at the origin of the world and I would rotate around a circle which radius is the distance of my object to the center of the world, hence drawing a big rainbow in the sky! Also I thought that would mean that my wheel will always stand up looking in the direction of the world z. (once I actually get forward motion I actually want to rotate the wheel on the y axis as the camera pans around)

If that’s a hint to the failure mode of my brain, I also don’t understand why there is basis and transform.basis as well as global_transform vs. transform.

You’re absolutely right about the conditions. I originally counted on the overshoot of the PID to take me to 90. I have changed the else to

if abs(rotation_degrees.z) <= TARGET_ANGLE + OFFSET_ANGLE and abs(rotation_degrees.z) >= TARGET_ANGLE - OFFSET_ANGLE:

so I can stand upright and rotate forward when I’m in the sweet spot.

Here is the PID code, the original version did the math on Vector3’s but I changed that floats as I’m dealing only with one axis:

# from https://www.youtube.com/watch?v=zTp7bWnlicY&t=183s
# Game Fabric - Rigid Body Character Controller in Godot 4 - Tutorial
# and
# https://www.youtube.com/watch?v=XfAt6hNV8XM
# Brian Douglas - Simple Examples of PID Control

extends RefCounted
class_name Pid1D

var _p : float
var _i : float
var _d : float

var _prevError : float
var _errorIntegral : float

func _init(p : float, i : float, d : float):
	_p = p
	_i = i
	_d = d

func update(error : float, delta: float) -> float:
	_errorIntegral += error * delta
	var _errorDerivative = (error - _prevError) / delta
	_prevError = error
	return _p * error + _i * _errorIntegral + _d * _errorDerivative

EDIT: Oh and the 0.03 is a naughty magic number to scale the influence of the PID which is itself instantiated outside of the _physics_process function with weights for Proportion, Integration and Differential:

var _pid1D := Pid1D.new(0.5, 0.6, 0.6)

Well, basis and transform.basis is the same thing – basis is just a shorthand. position is also a shorthand (for transform.origin).

As for why there is a transform and a global_transform, it’s simply that:

  • transform is the object’s transform in local space
  • global_transform is the object’s transform in global space

The transform of an object implicitly defines its position and orientation (and a few more things).


You’re not wrong. If you were to use the object’s global rotation to compute and apply torque, it would just make it turn around the axes of the world, not the object.

However, you forget to take into account the fact that your object travels via angular momentum. When an object has angular momentum, it rotates, and to rotate and object, its space is rotated. As soon as your object starts to travel along it’s z-axis, the basis of the object rotates and the z-axis now points in a different direction.

What you actually want is, not the z-axis vector, but the vector pointing forward.

“WELL, DUH!”

To get such a forward-facing vector, we can make use of both: the object’s local space, and the global space of the world. To start, let’s define two vectors:

  • var local_x = transform.basis.x
  • var global_y = Vector3.UP

local_x is the object’s x-axis (as visualized in your GIF), and global_y is a vector unrelated to the object that points up (i.e. Vector3(0, 1, 0)).

Taking the cross product of these two vectors will yield a vector pointing forward.

var local_x = basis.x
var global_y = Vector3.UP

var forward_vector = global_y.cross(local_x).normalized()
# The vector is normalized to get a vector with a length of 1

Wheel_AxisOrbitView
Wheel_AxisRotationView

It is this forward-facing vector you should be using to drive your PID controller – not the local z-axis (it was a valiant guess though).


Extending from RefCounted just allows Godot to perform reference counting and free up memory whenever an instance is no longer referenced anywhere.


In conclusion, you need to use a different vector to drive your system – a vector that always points forward. You already tweaked the if-statement so that should work. If it still doesn’t work after these changes, kindly post the updated code.

I hope that does it. Let me know if there is anything else.

EDIT: The PID script looks fine.

2 Likes

First of all I just want to say that I am amazed and grateful for the clarity, dedication and kind tone of your response!

Oooookay that makes so much more sense!

Could you say that:
global_transform
and
transform * $rootNode.basis
are the same thing?

Which leads me to wonder: what is the reference point for the value of the rotation variable? My theory was that it was a relative rotation compared to its own original rotation. And that because of that the other rotations didn’t matter. Like if I rotate a plane 90 degrees on one axis, I can rotate it on another and it will still be perpendicular to its old self:

square

Please don’t think I’m second guessing your explanation, I just want to untie that knot in my brain to be future-proof :slight_smile:

Ok. I actually remember reading that page and not fully understand what it entailed and that it would probably make sense when I’m at the optimisation stage of my code and I have to worry about memory…

Crystal clear. And again thank you for taking the time to explain and create visuals. I will try and implement this right now and get back with the results!

You can’t multiply a Transform3D by a Basis. In any case, if you’re trying to make the argument that the global transform is just the local transform rotated by its parent’s basis, you’d be incorrect – sort of.

In local space, the transform describes an object’s position and rotation relative to it’s immediate parent. In global space, the transform describes an object’s position and rotation relative to the world.

In Godot, and most other 3D software, objects can be parented to other objects. You may already know that objects in such relationships are referred to as child and parent. It is not uncommon for objects to exist in a large hierarchy of parents and children. For example:

  • Root
    • 1st
      • 2nd
        • 3rd
    • 1st
      • 2nd

Focusing on the 3rd object, it’s global transform is computed by adding the aggregate transformation changes of each ancestor. In this case, its ancestors are: 2nd, 1st, and Root. A code-equivalent would be:

var second: Transform3D
var first: Transform3D
var root: Transform3D

var global_tx = root * first * second * transform
# NOTE: I'm not sure whether this is the correct multiplication order.
#       I never multiply entire transforms.

I’m not sure whether this explanation also answers your other question. Your concern quoted below uses vague terms that are hard to argue for/against. If you don’t think the above explanation answers this concern, perhaps you could be more precise in your use of terms.

I do want to note that an object’s rotation and its basis vectors are two different things, in case that is what is confusing you.


You reap what you sow. You made your bed and now you get to lie in it. You get what you deserve. Karma’n’shit.


Keep on keeping on!

Relevant documentation page: Godot Docs | Transform3D * Transform3D

Oh staaahp it :blush:

I guess I just got confused by the fact that in the 3D editor, changing rotation on one axis doesn’t change the value of another. I have read your post very carefully, as well the linked docs and then some and it’s all starting to get clearer now.
I understand that, under the hood there is never a real rotation but matrix multiplication (and sines and cosines) of 3 vectors (well, 6) so the value of all vectors will have an influence on the resulting basis.

I have also been trying to implement your solution. I added another cross product to find a new x axis that is perpendicular to global_y and forward_vector to use as an “axle” for the forward torque. But the fact that _pid1D.update uses rotation_degrees.z as an argument still puts a spanner in the works (in my native language we actually say “puts a stick through the wheel” :thinking:). I feel like I should be able to do some math to create an absolute version rotation_degrees.z using the local z and the new forward_vector so I’m chasing that… I hope its not a red herring!

I don’t see why you would want a “new x axis”. Applying torque that is computed from a vector that deviates from your x-axis will introduce wobble. I doubt that’s your goal.


Taking my previous post into account, think about how your wheel has to “get up” in space. You’ll find that the forward_vector is the axis of rotation where torque must be applied – the z-rotation is irrelevant.


The first part to solving this problem is remembering what the z-rotation is meant to represent (even if it doesn’t). For your system, I believe the parameter for _pid1D should represent how far the object is from an upright orientation.

For most objects, an upright orientation can be defined as: a minimal difference between the y-axis of the world and the object.

var angle_to_up = basis.y.angle_to(Vector3.UP) # You CAN'T do this.

However, since you’re dealing with a wheel that spins, this typical definition doesn’t apply. The wheel’s Y-axis doesn’t implicitly describe what is up anymore. You have to create a new definition for what constitutes an upright orientation.

Since your wheel spins around the x-axis, it serves as a good indicator for it’s orientation. Therefore, we will use this in the new “upright definition”.

If you imagine your wheel in an upright position, you’ll notice that its x-axis is fairly parallel to the ground. Conversely, it points almost straight up or down when the wheel has “fallen over”. This observation is our new definition for what is considered upright.

The way to actually compute the “uprightness” from this new definition is a simple dot-product between the global y-axis, and the wheel’s x-axis. A value of 0 means that the wheel is upright, and a value of 1 or -1 constitutes the opposite.

var uprightness = basis.x.dot(Vector3.UP)
pid = _pid1D.update(uprightness, delta) * magic_number

This post isn’t quite as rigorous as I’d like with the mathematics. It’s fairly obvious that the dot-product and Vector3.angle_to() are pretty much one-and-the-same so I don’t know why I opted to use both – perhaps to illustrate their relation? Who knows. Hopefully I still got the point across.

I thought that I’d get the same issue with the x axis as I did with the z. But I’m not measuring an angle, just applying torque, and I understand that that new x axis won’t be perpendicular to my wheel so I’ll get wobble.

Yes. I definitely need to stop thinking in terms of angles but rather in terms of vectors and bases (which seems to be a plural both US and UK agree on!).

The more landscape I can see the better I can draw the map!

Thank a lot for bearing with. I usually consider myself smart enough, but for some reason this whole thing seems like trying to fit a square peg in a round hole. Usually when that happens it’s because there’s a core concept that I’m missing. In this case its probably the angles vs. vectors paradigm. I should’ve listened better in school…

Getting to work now, hopefully back with good news in a bit!

All right, still no luck. The forward_vector torque and basis_x are still somehow coupled and the pid keeps the wheel from spinning on x (well, I’ve actually realised the axle is on y once we’re upright.)

I’m pasting the code and the node tree in case you (or someone else!) still have patience with this. I have tried all possible permutations of axes and rewriting the conditions. I’ve tried a plain flat “ground” in case my terrain mesh was dodgy. I’m gonna sleep on it.

20241214_00h00m08s_grim

extends RigidBody3D

@onready var pivot = $SpringArmPivot
@onready var arm = $SpringArmPivot/SpringArm3D
@export var sens = 0.005

var _pid1D := Pid1D.new(1, 1, 1)
var pid
var TARGET_ANGLE = 1.0
var OFFSET_ANGLE = 0.1

var mouse_captured: bool
var inputDirection : Vector2 = Vector2(0, 0)

func _input(event):
	if event is InputEventMouseMotion:
		pivot.rotate_y(-event.relative.x * sens)
		arm.rotate_x(-event.relative.y * sens)

func _ready() -> void:
	can_sleep = false
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	mouse_captured = true

func _physics_process(delta: float) -> void:
	# Put the camera near the object
	$SpringArmPivot.global_transform.origin = global_transform.origin
	
	# Create a vector pointing forward from local x and global y
	var local_x = basis.x
	var global_y = Vector3.UP
	var forward_vector = global_y.cross(local_x).normalized()
	var uprightness = self.basis.x.dot(Vector3.UP)
	pid = _pid1D.update(uprightness - TARGET_ANGLE, delta) * 3
	
	# Get direction input from player
	inputDirection = Input.get_vector("move_l", "move_r", "move_f", "move_b")
	var debug_direction
	if inputDirection:
		# If the wheel rotation is below or above 90 degrees we apply a torque to keep it upright
		if uprightness < TARGET_ANGLE or uprightness > TARGET_ANGLE:
			# The PID controller tries to keep the rotation at 90 degrees to keep the wheel upright 
			apply_torque(forward_vector * pid)
			debug_direction = "rising"
		# If we're upright we roll forward
		if uprightness <= TARGET_ANGLE + OFFSET_ANGLE and uprightness >= TARGET_ANGLE - OFFSET_ANGLE:
			if inputDirection.y == -1 :
				apply_torque(-basis.y * 3)
				debug_direction = "forward"
	# Just for debugging:
	$CanvasLayer/Label.text = "%s %s %s %s" % [debug_direction, forward_vector, pid, basis.x]

Upon further inspection, I appear to have missed the fact that your wheel’s default transform has the wheel lying on its side instead of standing on its circumference (which I expected). This means that the fundamental reference frame – which are the axes of the wheel – are different between us. I believe that is why you have yet to make it work. We have not been talking about the same axes.

I realize now that this is plainly visible in the GIF of your original post.

I suggest you rotate the mesh and collision shape of your wheel such that the wheel is seen standing in the editor. The x-axis of the physics body should point right, the y-axis up, and the z-axis backwards. You can read more about Godot’s axes and their directionality here.

In addition, I think I might have confused myself after trying to explain the concept of space and the difference between local and global space.

As you might recall, I said that:

I just want to make clear that what I actually meant to say was: the space of the object, and the space of the world. This goes for the related code as well.

I fucked up! Sorry about that.


Aside from this unfortunate miscommunication, I also looked at the code.

I believe I have revised the script such that it works as you want.

Forum_SelfBalancingWheel_Demo

I’ve made some changes to the script but the overall approach is still the same. The script can be seen at the end of this post.

Walkthrough of changes

Change #1: Variable naming
Your current code defines a OFFSET_ANGLE that is used as an angular threshold in related if-statements. However, since the new approach uses the dot product to compute the uprightness, calling it an angle is misleading. Therefore, I’ve opted to rename it to UPRIGHT_THRESHOLD.
I’m not too satisfied with the term “uprightness” so if you think of a better way to communicate the concept, feel free to rename stuff to that.

Change #2: Upright-related conditions
Currently, you have a few conditions in your inputDirection if-block. The first is

if uprightness < TARGET_ANGLE or uprightness > TARGET_ANGLE:

which I have changed to

if abs(uprightness) > UPRIGHT_TRESHOLD:

I think you’re forgetting that the value range of uprightness that denotes an upright orientation is around 0. The condition should only evaluate to true when a certain threshold is crossed i.e. when the wheel is no longer upright enough. This threshold is controlled with UPRIGHT_THRESHOLD. Since uprightness is negative when the wheel falls on its right side, we use its absolute value. We don’t care about left/right, we just want the normalized distance from an upright orientation.

The other condition is

if uprightness <= TARGET_ANGLE + OFFSET_ANGLE and uprightness >= TARGET_ANGLE - OFFSET_ANGLE:

which has been changed to

if abs(uprightness) < UPRIGHT_TRESHOLD:

This is very similar to the other change, we just use < instead of >. We do this because you, presumably, only want the wheel to rotate when it is upright enough.

The code that computes the forward vector is also changed in response to the axis-misunderstanding.

	# Create a forward-vector from the object's x-axis and the global y-axis
	var object_x = global_basis.x
	var world_y = Vector3.UP
	var forward_vector = world_y.cross(object_x).normalized()

Finally, I decided to not use the PID controller. I don’t have much experience with PID controllers so I don’t know whether it’s actually a good use case for it. Instead, the balancing torque is now scaled by the uprightness as well as an @export variable named BALANCING_TORQUE_STRENGTH.

apply_torque(forward_vector * uprightness * BALANCE_TORQUE_STRENGTH)
extends RigidBody3D

@onready var pivot = $SpringArmPivot
@onready var arm = $SpringArmPivot/SpringArm3D
@export var sens = 0.005

var _pid1D := Pid1D.new(1, 1, 1)
var pid
@export_range(0, 1) var UPRIGHT_TRESHOLD = 0.2
@export_range(0,45) var TARGET_ANGLE = 0
@export var BALANCE_TORQUE_STRENGTH = 10.0
@export var ACCELERATION_STRENGTH = 3.0

var mouse_captured: bool
var input_direction : Vector2 = Vector2(0, 0)

func _input(event):
	if event is InputEventMouseMotion:
		pivot.rotate_y(-event.relative.x * sens)
		arm.rotate_x(-event.relative.y * sens)

func _ready() -> void:
	can_sleep = false
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	mouse_captured = true

func _physics_process(delta: float) -> void:
	# Put the camera near the object
	pivot.global_transform.origin = global_transform.origin
	
	# Create a forward-vector from the object's x-axis and the global y-axis
	var object_x = global_basis.x
	var world_y = Vector3.UP
	var forward_vector = world_y.cross(object_x).normalized()
	
	# Compute the normalized distance from an 'upright' orientation
	var uprightness = object_x.dot(world_y)
	pid = _pid1D.update(uprightness - (TARGET_ANGLE / 90.0), delta) * 3
	
	# Get direction input from player
	input_direction = Input.get_vector("steer_left", "steer_right", "brake", "accelerate")
	var debug_direction = ""
	if input_direction:
		# If the wheel is far from an upright position, apply balancing torque.
		if abs(uprightness) > UPRIGHT_TRESHOLD:
			apply_torque(forward_vector * uprightness * BALANCE_TORQUE_STRENGTH)
			debug_direction = "rising"
		# If we're upright we roll forward
		if abs(uprightness) < UPRIGHT_TRESHOLD:
			if input_direction.y != 0:
				# Apply torque on the x-axis scaled by the user's input
				apply_torque(object_x * ACCELERATION_STRENGTH * input_direction.y)
				debug_direction = "forward"
	
	# DELETE THIS - it's project-specific debugging code.
	DebugGeo.draw_debug_line(delta, global_position, global_position + forward_vector, 0.01, Color.BLUE)
	DebugGeo.draw_debug_line(delta, global_position, global_position + basis.x * (0.15 + uprightness), 0.01, Color.RED)


This better work for you as well! Let me know if anything is incorrect or you have any questions.

1 Like

You’re an absolute rockstar! It does work for me too :slight_smile:

I did have a hunch that the axis swap might be causing issues. I actually mentioned that in passing in my last post. I should have made more of a point of it, which could have saved you time troubleshooting my issue. Sorry!

I have tried to experiment with “reversing” the changes that you’ve made one by one to pinpoint exactly where the problem lied:

Both basis.x and global_basis.x work when calculating forward_vector (if I print their values, they are actually the same). Is that because we are scripting the root node of the wheel scene so the parent is also the world?

Reinstating the PID also works.

Using a non absolute value for the condition also works (except that I lose the ability to stand if I fall to the negative side which makes sense).

So it seems that the initial orientation was the culprit. I am still confused as to why I wasn’t able to accommodate for it by calculating forward_vector using basis.y or global_basis.y and Vector3.UP in that case and apply torque on the Y axis to spin the wheel forward.
→ Which I just tried and it works :face_with_raised_eyebrow:

So basically I’m pretty sure I’ve tried to revers all the changes and I can’t reproduce the original issue. Oh well, it seems like you just blessed my code!

Anyway. You have sorted me out and I am extremely grateful. I’m only asking questions and fiddling about because I’d like this to be about learning to fish rather than being given fish. Feel free to ignore me as I know you’re busy!

Here is an updated version. I’ve added the ability to steer and got rid of the condition that prevents rolling forward unless we’re upright as I’d rather a bit of an “unbalanced” start than loosing propulsion every time it gets a bit out of balance.

If this project ever makes it out of my machine I will make sure to credit you (as well as all the other learning resources that I’ve used). If it matters to you let me know in a DM how you’d like to be named!

extends RigidBody3D

# Camera motion variables
@onready var pivot = $SpringArmPivot
@onready var arm = $SpringArmPivot/SpringArm3D
@export var sens = 0.005

# Wheel control variables
@export_range(0, 1) var UPRIGHT_TRESHOLD = 0.2
@export_range(0,45) var TARGET_ANGLE = 0
@export var BALANCE_TORQUE_STRENGTH = 10.0
@export var ACCELERATION_STRENGTH = 3.0
@export var TURNING_STRENGTH = 3.0

# Mouse / keyboard input variables
var mouse_captured: bool
var input_direction : Vector2 = Vector2(0, 0)

func _input(event):
	# Rotate the camera according to mouse motion
	if event is InputEventMouseMotion:
		pivot.rotate_y(-event.relative.x * sens)
		arm.rotate_x(-event.relative.y * sens)

func _ready() -> void:
	can_sleep = false
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	mouse_captured = true

func _physics_process(delta: float) -> void:
	# Put the camera pivot on the object
	pivot.global_transform.origin = global_transform.origin
	
	# Create a forward-vector from the object's x-axis and the global y-axis
	var object_x = global_basis.x
	var world_y = Vector3.UP
	var forward_vector = world_y.cross(object_x).normalized()
	
	# Compute the normalized distance from an 'upright' orientation
	var upright_error = object_x.dot(world_y)
	
	# Get direction input from player
	input_direction = Input.get_vector("move_l", "move_r", "move_f", "move_b")
	if input_direction:
		# If the wheel is far from an upright position, apply balancing torque.
		if abs(upright_error) > UPRIGHT_TRESHOLD:
			apply_torque(forward_vector * upright_error * BALANCE_TORQUE_STRENGTH)
		# Steer the wheel left or right
		apply_torque_impulse(-world_y * input_direction.x * TURNING_STRENGTH * delta)
		# Roll forward or backwards if one of these directions is pressed
		if input_direction.y != 0:
			# Apply torque on the x-axis scaled by the user's input
			apply_torque(object_x * ACCELERATION_STRENGTH * input_direction.y)

1 Like

I don’t think I can provide an adequate explanation that would give you a sense of the difference, and it wouldn’t be any different than what I’ve already said regarding space. Read the relevant section of that post again if you like. Additionally, you may watch 3Blue1Brown’s playlist on Linear Algebra which provides a good visual depiction of space transformations and other subjects in that topic: Essence of Linear Algebra.


You may want to change the way you apply your steering torque. Impulses are reserved for instantly applied force/torque e.g. collision or constraint forces. Scaling the impulse by delta is essentially the same as using apply_torque. Unless you have a reason to use an impulse here, I suggest you change it to:

apply_torque(-world_y * input_direction.x * TURNING_STRENGTH)

If you run into issues with steering on hills, consider using the normal as the turning axis when grounded and falling back on using the world’s y-axis when airborne.


I’m happy it finally works. Good luck with the project rolling forward!

1 Like

I see what you did here :smirk:

I have been watching it. His videos are amazing! I think I do understand the difference now.

Noted. :pray:

Yes, the wheel tends to try and turn away from the upwards direction on hills. I did think of experimenting with normals to solve that :nerd_face: Please don’t give me anymore clues :heart: I know things are going to click-in over the next few days.

The whole basis manipulation process is way clearer to me now. Refining the motion of the wheel will be a good way to integrate and practice all the new knowledge that I’ve acquired during our exchanges. Thanks again, hope karma gives it all back to you!

Best wishes!

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.