Add Input.get_device_orientation() returning hardware-fused quaternion#119142
Add Input.get_device_orientation() returning hardware-fused quaternion#119142lxdua wants to merge 10 commits into
Input.get_device_orientation() returning hardware-fused quaternion#119142Conversation
Exposes the system-level fused device orientation as a Quaternion via a new Input.get_device_orientation() method, complementing the existing raw gyroscope/gravity/accelerometer accessors. Backed by hardware sensor fusion on mobile platforms: - Android: TYPE_GAME_ROTATION_VECTOR (preferred, no magnetometer drift), with TYPE_ROTATION_VECTOR as a fallback. The quaternion produced by SensorManager.getQuaternionFromVector() is corrected for the current screen rotation via pre-computed quaternion multiplication, then remapped from the Android sensor coordinate system to Godot's convention at the platform bridge layer. - iOS / Apple Embedded: CMDeviceMotion.attitude.quaternion, with per-screen-orientation correction quaternions. - Desktop and other platforms: returns Quaternion() (identity). Gated by a new project setting, disabled by default to save battery: input_devices/sensors/enable_device_orientation = false Tested on a Redmi 2602BRT18C (Android, arm64) in landscape mode: 1:1 match between physical device rotation and the returned quaternion, no visible jitter, no drift over ~5 minutes of continuous use.
Address pre-commit suggestions from CI.
make_rst expects <method> and <member> entries to be sorted by name.
Input.get_device_orientation() returning hardware-fused quaternion
@lxdua Can you provide the sample you used to validate the API so others can replicate on other devices. |
| // Pre-computed screen rotation correction quaternions (around Z axis). | ||
| // Format: { w, x, y, z } |
There was a problem hiding this comment.
Can you document where these values are coming from?
A single-script 3D scene that creates a virtual phone model at runtime — no scene file needed: extends Node3D
## ------------------------------------------------------------------
## Sensor orientation PR test scene.
##
## 1. Project Settings → Display → Window → Handheld → Orientation = Sensor.
## 2. Turn OFF the device's system-level rotation lock.
## 3. Tap "Reset" to calibrate. Virtual phone should track physical
## pitch / roll consistently in every orientation the device OS
## actually switches to.
##
## The `Viewport:` line already tells you portrait vs. landscape, so
## there's no explicit coverage UI — just physically rotate and confirm
## behavior in every state the device will let you enter.
##
## If you suspect the pre-rotation is causing axis confusion in
## landscape, flip USE_BASE_ORIENTATION to false; the mesh will always
## render portrait but the raw motion axes should match 1:1.
## ------------------------------------------------------------------
const USE_BASE_ORIENTATION := false # Set true to pre-rotate mesh into current layout.
var phone_mesh: MeshInstance3D
var label: Label
var reset_button: Button
var reference_inv := Quaternion.IDENTITY
var base_orientation := Quaternion.IDENTITY
var calibrated := false
func _ready() -> void:
var camera := Camera3D.new()
camera.position = Vector3(0, 0, 3)
add_child(camera)
camera.look_at(Vector3.ZERO)
camera.current = true
var light := DirectionalLight3D.new()
light.rotation_degrees = Vector3(-45, 30, 0)
add_child(light)
phone_mesh = MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(0.8, 1.4, 0.1)
phone_mesh.mesh = box
var body_mat := StandardMaterial3D.new()
body_mat.albedo_color = Color(0.2, 0.5, 1.0)
phone_mesh.set_surface_override_material(0, body_mat)
add_child(phone_mesh)
var cam_dot := _make_sphere(0.06, Color(1.0, 0.2, 0.2))
cam_dot.position = Vector3(0, 0.55, 0.06)
phone_mesh.add_child(cam_dot)
var back_dot := _make_sphere(0.06, Color(0.2, 1.0, 0.2))
back_dot.position = Vector3(0, 0.55, -0.06)
phone_mesh.add_child(back_dot)
var right_dot := _make_sphere(0.05, Color(1.0, 0.9, 0.1))
right_dot.position = Vector3(0.45, 0, 0.06)
phone_mesh.add_child(right_dot)
var axes := _make_axes()
axes.position = Vector3(-0.8, -0.6, 0)
axes.scale = Vector3.ONE * 0.35
add_child(axes)
var control := Control.new()
control.set_anchors_preset(Control.PRESET_FULL_RECT)
control.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(control)
label = Label.new()
label.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
label.offset_top = -180
label.offset_bottom = -16
label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 22)
label.add_theme_color_override("font_outline_color", Color.BLACK)
label.add_theme_constant_override("outline_size", 4)
control.add_child(label)
reset_button = Button.new()
reset_button.text = "Reset"
reset_button.add_theme_font_size_override("font_size", 28)
reset_button.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
reset_button.offset_left = -100
reset_button.offset_right = 100
reset_button.offset_top = -276
reset_button.offset_bottom = -196
reset_button.pressed.connect(_on_reset_pressed)
control.add_child(reset_button)
func _make_sphere(radius: float, color: Color) -> MeshInstance3D:
var m := MeshInstance3D.new()
var s := SphereMesh.new()
s.radius = radius
s.height = radius * 2.0
m.mesh = s
var mat := StandardMaterial3D.new()
mat.albedo_color = color
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
m.set_surface_override_material(0, mat)
return m
func _make_axes() -> Node3D:
var root := Node3D.new()
var colors := [Color.RED, Color.GREEN, Color.BLUE]
var dirs := [Vector3.RIGHT, Vector3.UP, Vector3.BACK]
for i in range(3):
var rod := MeshInstance3D.new()
var cyl := CylinderMesh.new()
cyl.top_radius = 0.02
cyl.bottom_radius = 0.02
cyl.height = 0.6
rod.mesh = cyl
var mat := StandardMaterial3D.new()
mat.albedo_color = colors[i]
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
rod.set_surface_override_material(0, mat)
rod.position = dirs[i] * 0.3
if dirs[i] == Vector3.RIGHT:
rod.rotation_degrees = Vector3(0, 0, -90)
elif dirs[i] == Vector3.BACK:
rod.rotation_degrees = Vector3(90, 0, 0)
root.add_child(rod)
return root
func _is_landscape() -> bool:
var size := get_window().size
return size.x > size.y
func _screen_to_base_rotation() -> Quaternion:
if not USE_BASE_ORIENTATION:
return Quaternion.IDENTITY
if _is_landscape():
return Quaternion(Vector3(0, 0, 1), deg_to_rad(90))
return Quaternion.IDENTITY
func _on_reset_pressed() -> void:
var raw := Input.get_device_orientation()
reference_inv = raw.inverse()
base_orientation = _screen_to_base_rotation()
calibrated = true
func _process(_delta: float) -> void:
var raw: Quaternion = Input.get_device_orientation()
var shown: Quaternion = reference_inv * raw if calibrated else raw
phone_mesh.quaternion = shown * base_orientation
var e := shown.get_euler()
var raw_e := raw.get_euler()
var vp := get_window().size
label.text = ("%s\nViewport: %dx%d [%s]\n\n"
+ "Shown P=%6.1f° Y=%6.1f° R=%6.1f°\n"
+ "Raw P=%6.1f° Y=%6.1f° R=%6.1f°\n"
+ "Raw q (w=%.3f, x=%.3f, y=%.3f, z=%.3f)") % [
"[Calibrated]" if calibrated else "[Press Reset to calibrate]",
vp.x, vp.y, "LANDSCAPE" if _is_landscape() else "PORTRAIT",
rad_to_deg(e.x), rad_to_deg(e.y), rad_to_deg(e.z),
rad_to_deg(raw_e.x), rad_to_deg(raw_e.y), rad_to_deg(raw_e.z),
raw.w, raw.x, raw.y, raw.z,
]
|
| if (values.length < 4) { | ||
| return; | ||
| } | ||
| float[] quaternion = new float[4]; |
There was a problem hiding this comment.
You should avoid allocating a new object each time. Instead store it as a class field and reuse it each time.
| * Each quaternion is { w, x, y, z }. | ||
| */ | ||
| private static float[] quaternionMultiply(float[] a, float[] b) { | ||
| return new float[] { |
There was a problem hiding this comment.
Don't return a new float, instead take an additional float[] parameter to be used to return the result.
e8015d0 to
15ede5e
Compare
Avoid per-event allocations in the TYPE_(GAME_)ROTATION_VECTOR branch of onSensorChanged() by hoisting the quaternion scratch buffers to instance fields and converting quaternionMultiply() to an alias-safe out-parameter form. The callback can fire at 50-200 Hz, so dropping the two new float[4] allocations per event avoids avoidable GC pressure. Also expand the documentation around the rotation correction table: derive each entry from q(θ) = ( cos(θ/2), 0, 0, sin(θ/2) ), explain the q_screen_in_world = q_screen_in_device * q_device_in_world framing, and note the Android rotation-vector identity pose (flat, screen up, long edge toward magnetic north) so callers are not surprised by non-trivial quaternions when the device feels 'still'.
…Godot.kt Change "mRotationVector" to "rotationVector" Co-authored-by: Fredia Huya-Kouadio <fhuyakou@gmail.com>
f1f2603 to
0a512b1
Compare
…estion The previous commit renamed the field declaration only; this fixes the two remaining call-sites in registerListeners() that still referred to the old name, which was breaking the Kotlin compile.
|
During the testing process, I discovered that Godot does not yet have APIs for asking the portrait and landscape screen orientations of mobile devices. This seems to be another area that needs to be addressed.🫤 |
Co-authored-by: A Thousand Ships <96648715+AThousandShips@users.noreply.github.com>
Co-authored-by: A Thousand Ships <96648715+AThousandShips@users.noreply.github.com>
m4gr3d
left a comment
There was a problem hiding this comment.
The Android logic looks good.
e2cda7a to
c4dd543
Compare
|
Tested on a Pixel 10 Fold using the script from #119142 (comment) and it works as expected! screen-20260505-102718-1778002015269.mp4 |
Both PR godotengine#119142 (device orientation sensor) and upstream (joystick touchpad functions) added methods in the same location after get_gyroscope(). Keep both additions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes godotengine/godot-proposals/issues/14704
Summary
Adds
Input.get_device_orientation()returning aQuaternionrepresenting thedevice's absolute orientation, sourced from system-level hardware sensor fusion
on mobile platforms. Fills the gap between Godot and Unity's
Input.gyro.attitude.Implementation
Android:
TYPE_GAME_ROTATION_VECTOR(preferred) →TYPE_ROTATION_VECTOR(fallback). The quaternion from
SensorManager.getQuaternionFromVector()iscorrected for the current screen rotation, then mapped from the Android sensor
coordinate system to Godot's convention at the
DisplayServerAndroidbridge.iOS / Apple Embedded:
CMDeviceMotion.attitude.quaternion, with per-screen-orientation correction quaternions in
godot_view_apple_embedded.mm.Other platforms: returns
Quaternion()(identity).API
Input.get_device_orientation() -> Quaternioninput_devices/sensors/enable_device_orientation(defaultfalse)doc/classes/Input.xmlanddoc/classes/ProjectSettings.xmlTesting
Verified on a Redmi Turbo 5 Max (Android, arm64) in landscape:
Default behavior
Disabled by default (battery-friendly), opt-in via project setting — identical
to the existing
enable_gyroscope/enable_magnetometerpattern.