Repository: godotengine/godot (language: cpp)

Issue:
get_rpm() on VehicleWheel3D is calculated incorrectly
### Tested versions

- Reproducible in: 4.2 stable, 4.3 stable, 4.4.dev7, 4.4 stable

### System information

Godot v4.3.stable (77dcf97d8) - Windows 10.0.26100 - Vulkan (Forward+) - integrated Intel(R) UHD Graphics (Intel Corporation; 31.0.101.2130) - Intel(R) Core(TM) i7-10610U CPU @ 1.80GHz (8 Threads)

### Issue description

When you call get_rpm on a VehicleWheel3D when the steering angle is not equal to 0, it returns the wrong RPM. The only workaround I have found is to use trigonometry:
```gdscript
var corrected_rpm = wheel.get_rpm() / cos(wheel.steering)
```

### Steps to reproduce

- Create a VehicleBody3D
- Create a VehicleWheel3D as a child
- Spin up the wheel (engine_force or with friction against the ground)
- Turn the wheel with steering on the VehicleBody3D
- Call get_rpm() on the VehicleWheel3D which now gives the wrong rpm

### Minimal reproduction project (MRP)

[vehicle-bug.zip](https://github.com/user-attachments/files/18447812/vehicle-bug.zip)


Discussion / hints:
After some digging it looks like the vehicle implementation is heavily based on the [Bullet Physics vehicle](https://github.com/bulletphysics/bullet3/blob/master/src/BulletDynamics/Vehicle/btRaycastVehicle.cpp). Bullet has the exact same broken code as godot:

```cpp
	for (int i = 0; i < wheels.size(); i++) {
		VehicleWheel3D &wheel = *wheels[i];
		Vector3 relpos = wheel.m_raycastInfo.m_hardPointWS - p_state->get_transform().origin;
		Vector3 vel = p_state->get_linear_velocity() + (p_state->get_angular_velocity()).cross(relpos); // * mPos);

		if (wheel.m_raycastInfo.m_isInContact) {
			const Transform3D &chassisWorldTransform = p_state->get_transform();

			Vector3 fwd(
					chassisWorldTransform.basis[0][Vector3::AXIS_Z],
					chassisWorldTransform.basis[1][Vector3::AXIS_Z],
					chassisWorldTransform.basis[2][Vector3::AXIS_Z]);

			real_t proj = fwd.dot(wheel.m_raycastInfo.m_contactNormalWS);
			fwd -= wheel.m_raycastInfo.m_contactNormalWS * proj;

			real_t proj2 = fwd.dot(vel);
			
			wheel.m_deltaRotation = (proj2 * step) / (wheel.m_wheelRadius);
		}

		wheel.m_rotation += wheel.m_deltaRotation;
		wheel.m_rpm = ((wheel.m_deltaRotation / step) * 60) / Math_TAU;

		wheel.m_deltaRotation *= real_t(0.99); //damping of rotation when not in contact
	}
```

As for how to fix it I am still trying to figure out. This code also does not spin up the wheel when off the ground which should also happen when there is engine_power.

Fix patch:
diff --git a/scene/3d/physics/vehicle_body_3d.cpp b/scene/3d/physics/vehicle_body_3d.cpp
index c5c9153059d5..c508d4ff4ffe 100644
--- a/scene/3d/physics/vehicle_body_3d.cpp
+++ b/scene/3d/physics/vehicle_body_3d.cpp
@@ -852,18 +852,26 @@ void VehicleBody3D::_body_state_changed(PhysicsDirectBodyState3D *p_state) {
 	for (int i = 0; i < wheels.size(); i++) {
 		VehicleWheel3D &wheel = *wheels[i];
 		Vector3 relpos = wheel.m_raycastInfo.m_hardPointWS - p_state->get_transform().origin;
-		Vector3 vel = p_state->get_linear_velocity() + (p_state->get_angular_velocity()).cross(relpos); // * mPos);
+		Vector3 vel = p_state->get_linear_velocity() + (p_state->get_angular_velocity()).cross(relpos);
 
 		if (wheel.m_raycastInfo.m_isInContact) {
 			const Transform3D &chassisWorldTransform = p_state->get_transform();
 
+			// Get forward vector.
 			Vector3 fwd(
 					chassisWorldTransform.basis[0][Vector3::AXIS_Z],
 					chassisWorldTransform.basis[1][Vector3::AXIS_Z],
 					chassisWorldTransform.basis[2][Vector3::AXIS_Z]);
 
+			// Apply steering rotation to forward vector for steerable wheels.
+			if (wheel.steers) {
+				Basis steering_mat(Vector3(0, 1, 0), wheel.m_steering);
+				fwd = steering_mat.xform(fwd);
+			}
+
 			real_t proj = fwd.dot(wheel.m_raycastInfo.m_contactNormalWS);
 			fwd -= wheel.m_raycastInfo.m_contactNormalWS * proj;
+			fwd.normalize();
 
 			real_t proj2 = fwd.dot(vel);
 


Test patch: