Skip to content
Snippets Groups Projects
Commit 42f6821f authored by grosglob's avatar grosglob
Browse files

Added xr navigation with controllers

parent 045a6597
Branches
Tags
No related merge requests found
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
# <pep8 compliant> # <pep8 compliant>
import bpy import bpy
from mathutils import Matrix, Quaternion, Vector
from bpy.types import ( from bpy.types import (
Gizmo, Gizmo,
GizmoGroup, GizmoGroup,
...@@ -144,7 +145,7 @@ def xr_landmark_active_update(self, context): ...@@ -144,7 +145,7 @@ def xr_landmark_active_update(self, context):
xr_landmark_active_base_pose_angle_update(self, context) xr_landmark_active_base_pose_angle_update(self, context)
if wm.xr_session_state: if wm.xr_session_state:
wm.xr_session_state.reset_to_base_pose(context) wm.xr_session_state.reset_to_base_pose(context)
class VRLandmark(bpy.types.PropertyGroup): class VRLandmark(bpy.types.PropertyGroup):
...@@ -500,6 +501,148 @@ classes = ( ...@@ -500,6 +501,148 @@ classes = (
VIEW3D_GGT_vr_viewer_pose, VIEW3D_GGT_vr_viewer_pose,
) )
# this class manages VR navigation using VR controllers.
# the Fly navigation allows user to manipulate the scene using the left controller (world scale is modified using left joystick).
# The Bi-manual navigation is the standard VR navigation with 2 controllers.
class Navigation:
griped = False
init_pivot_invert_matrix = None
init_world_matrix = None
scale = 1.0
init_distance = 0.0
navigation_mode = "Bimanual"
fixedScaleFactor = 1.05
hysteresis_min = 0.3
hysteresis_max = 0.5
def navigate(self):
try:
# trys to get an openxr session state
wm = bpy.context.window_manager
if not wm.xr_session_state:
return
session_state = wm.xr_session_state
if self.navigation_mode == "Fly":
self.navigation_fly(session_state)
if self.navigation_mode == "Bimanual":
self.navigation_bimanual(session_state)
except:
# openxr is not ready
pass
def set_mode(self, mode):
self.griped = False
self.navigation_mode = mode
def navigation_fly(self, session_state):
grip = session_state.left_grip_value
justGriped = False
if not self.griped and grip > self.hysteresis_max:
# left grip is pressed
self.griped = True
justGriped = True
else:
if self.griped and grip < self.hysteresis_min:
# left grip is released
self.griped = False
if self.griped:
# get left controller matrix
controller_translate = session_state.left_controller_location
controller_rotate = session_state.left_controller_rotation
controller_matrix = Matrix.Translation(controller_translate) @ controller_rotate.to_matrix().to_4x4()
if justGriped:
# get init pivot and world matrix
self.scale = 1.0
self.init_pivot_invert_matrix = controller_matrix.inverted()
self.init_world_matrix = Matrix.Translation(session_state.world_location) @ session_state.world_rotation.to_matrix().to_4x4() @ Matrix.Scale(session_state.world_scale, 4)
if self.griped:
# compute scale from joystick y value
joyvalue = session_state.left_thumbstick_y
if joyvalue > self.hysteresis_max:
self.scale *= self.fixedScaleFactor
if joyvalue < -self.hysteresis_min:
self.scale /= self.fixedScaleFactor
# compute new fake world matrix
scale_matrix = Matrix.Scale(self.scale, 4)
transformedWorld = controller_matrix @ scale_matrix @ self.init_pivot_invert_matrix @ self.init_world_matrix
# apply to fake world transform
session_state.world_location = transformedWorld.to_translation()
session_state.world_rotation = transformedWorld.to_quaternion()
session_state.world_scale = transformedWorld.to_scale().x
def navigation_bimanual(self, session_state):
# get controllers grip values
left_grip = session_state.left_grip_value
right_grip = session_state.right_grip_value
justGriped = False
if not self.griped and left_grip > self.hysteresis_max and right_grip > self.hysteresis_max:
# if both grips are pressed
self.griped = True
justGriped = True
else:
if self.griped and (left_grip < self.hysteresis_min or right_grip < self.hysteresis_min):
# if both grips are released
self.griped = False
if self.griped:
# get position of the center of the controllers
center_position = (session_state.left_controller_location + session_state.right_controller_location) * 0.5
# get vector from left controller to right controller
delta = session_state.right_controller_location - session_state.left_controller_location
# compute a controller matrix centerd between the controllers
# and oriented by projecting vector from left controller to right controller to a horizontal plane
right = delta.normalized()
up = Vector((0,0,1))
forward = Vector.cross(up, right)
right = Vector.cross(forward, up)
# create pivot matrix from this base
pivot_matrix = Matrix()
pivot_matrix.col[0] = Vector((right.x, right.y, right.z, 0.0))
pivot_matrix.col[1] = Vector((forward.x, forward.y, forward.z, 0.0))
pivot_matrix.col[2] = Vector((up.x, up.y, up.z, 0.0))
pivot_matrix.col[3] = Vector((center_position.x, center_position.y, center_position.z, 1.0))
if justGriped:
# get init pivot and world matrix
self.griped = True
self.scale = 1.0
self.init_distance = delta.length
self.init_pivot_invert_matrix = pivot_matrix.inverted()
self.init_world_matrix = Matrix.Translation(session_state.world_location) @ session_state.world_rotation.to_matrix().to_4x4() @ Matrix.Scale(session_state.world_scale, 4)
if self.griped:
# compute scale
distance = delta.length
self.scale = distance / self.init_distance
# compute new fake world matrix
scale_matrix = Matrix.Scale(self.scale, 4)
transformedWorld = pivot_matrix @ scale_matrix @ self.init_pivot_invert_matrix @ self.init_world_matrix
# apply to fake world transform
session_state.world_location = transformedWorld.to_translation()
session_state.world_rotation = transformedWorld.to_quaternion()
session_state.world_scale = transformedWorld.to_scale().x
navigation = Navigation()
@persistent
def controller_handler():
global navigation
navigation.navigate()
return 0.0
@persistent
def register_controller_handler(context: bpy.context):
bpy.app.timers.register(controller_handler)
def register(): def register():
if not bpy.app.build_options.xr_openxr: if not bpy.app.build_options.xr_openxr:
...@@ -525,7 +668,8 @@ def register(): ...@@ -525,7 +668,8 @@ def register():
) )
bpy.app.handlers.load_post.append(ensure_default_vr_landmark) bpy.app.handlers.load_post.append(ensure_default_vr_landmark)
bpy.app.handlers.load_post.append(register_controller_handler)
bpy.app.timers.register(controller_handler)
def unregister(): def unregister():
if not bpy.app.build_options.xr_openxr: if not bpy.app.build_options.xr_openxr:
...@@ -539,8 +683,9 @@ def unregister(): ...@@ -539,8 +683,9 @@ def unregister():
del bpy.types.Scene.vr_landmarks_active del bpy.types.Scene.vr_landmarks_active
del bpy.types.View3DShading.vr_show_virtual_camera del bpy.types.View3DShading.vr_show_virtual_camera
bpy.app.timers.unregister(controller_handler)
bpy.app.handlers.load_post.remove(register_controller_handler)
bpy.app.handlers.load_post.remove(ensure_default_vr_landmark) bpy.app.handlers.load_post.remove(ensure_default_vr_landmark)
if __name__ == "__main__": if __name__ == "__main__":
register() register()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment