From dde9d21b91c455f029b1aabf41d131bd1652a534 Mon Sep 17 00:00:00 2001 From: "hogan.mastanduno" Date: Wed, 1 Oct 2025 22:16:12 +0200 Subject: [PATCH] OpenXR: VR Advanced Locomotion Phase 1 Includes the following changes to the existing Locomotion system for VR Scene Inspection: * new VR Navigation Preferences and VR Session Settings * changes to XR raycast logic and its visualization * new XR vignette that appears when moving * snap turning Pull Request: https://projects.blender.org/blender/blender/pulls/144241 --- release/datafiles/userdef/userdef_default.c | 16 + .../viewport_vr_preview/__init__.py | 5 +- .../viewport_vr_preview/configs/default.py | 54 +- .../viewport_vr_preview/defaults.py | 86 +++ .../addons_core/viewport_vr_preview/gui.py | 4 +- .../viewport_vr_preview/preferences.py | 67 ++ .../blender/blenkernel/BKE_blender_version.h | 2 +- .../blenloader/intern/versioning_500.cc | 6 + .../blenloader/intern/versioning_defaults.cc | 7 + .../blenloader/intern/versioning_userdef.cc | 7 + .../engines/overlay/overlay_background.hh | 27 + .../draw/engines/overlay/overlay_instance.cc | 6 + .../draw/engines/overlay/overlay_private.hh | 2 + .../shaders/infos/overlay_background_infos.hh | 3 + .../shaders/overlay_background_frag.glsl | 30 +- .../editors/include/ED_view3d_offscreen.hh | 1 + .../editors/space_view3d/view3d_draw.cc | 2 + source/blender/gpu/CMakeLists.txt | 2 + source/blender/gpu/GPU_shader_builtin.hh | 3 + source/blender/gpu/GPU_xr_defines.hh | 11 + .../blender/gpu/intern/gpu_shader_builtin.cc | 2 + .../shaders/gpu_shader_xr_raycast_vert.glsl | 43 ++ .../infos/gpu_shader_xr_raycast_infos.hh | 33 + source/blender/makesdna/DNA_userdef_types.h | 15 + source/blender/makesdna/DNA_view3d_types.h | 3 +- source/blender/makesdna/DNA_xr_types.h | 4 + source/blender/makesdna/intern/dna_defaults.c | 1 + source/blender/makesrna/intern/rna_userdef.cc | 44 ++ source/blender/makesrna/intern/rna_xr.cc | 10 +- source/blender/windowmanager/CMakeLists.txt | 1 + source/blender/windowmanager/WM_api.hh | 3 + .../blender/windowmanager/xr/intern/wm_xr.cc | 2 + .../windowmanager/xr/intern/wm_xr_draw.cc | 5 +- .../windowmanager/xr/intern/wm_xr_intern.hh | 24 + .../xr/intern/wm_xr_operators.cc | 714 ++++++++++++++---- .../windowmanager/xr/intern/wm_xr_session.cc | 70 ++ 36 files changed, 1146 insertions(+), 169 deletions(-) create mode 100644 scripts/addons_core/viewport_vr_preview/preferences.py create mode 100644 source/blender/gpu/GPU_xr_defines.hh create mode 100644 source/blender/gpu/shaders/gpu_shader_xr_raycast_vert.glsl create mode 100644 source/blender/gpu/shaders/infos/gpu_shader_xr_raycast_infos.hh diff --git a/release/datafiles/userdef/userdef_default.c b/release/datafiles/userdef/userdef_default.c index 4b9ceb18bad..7f87c480a9d 100644 --- a/release/datafiles/userdef/userdef_default.c +++ b/release/datafiles/userdef/userdef_default.c @@ -208,6 +208,14 @@ const UserDef U_default = { .flag = 0, }, + .xr_navigation = + { + .vignette_intensity = 60.0f, + .turn_amount = DEG2RAD(30), + .turn_speed = DEG2RAD(60), + .flag = USER_XR_NAV_SNAP_TURN, + }, + .space_data = { .section_active = USER_SECTION_INTERFACE, @@ -246,4 +254,12 @@ const UserDef U_default = { { .is_dirty = 0, }, + + .xr_navigation = + { + .vignette_intensity = 60.0f, + .turn_amount = DEG2RAD(30), + .turn_speed = DEG2RAD(60), + .flag = USER_XR_NAV_SNAP_TURN, + }, }; diff --git a/scripts/addons_core/viewport_vr_preview/__init__.py b/scripts/addons_core/viewport_vr_preview/__init__.py index 7418aec3be1..bcaff6bfc20 100644 --- a/scripts/addons_core/viewport_vr_preview/__init__.py +++ b/scripts/addons_core/viewport_vr_preview/__init__.py @@ -24,8 +24,9 @@ if "bpy" in locals(): importlib.reload(gui) importlib.reload(operators) importlib.reload(properties) + importlib.reload(preferences) else: - from . import action_map, gui, operators, properties + from . import action_map, gui, operators, properties, preferences import bpy @@ -39,6 +40,7 @@ def register(): gui.register() operators.register() properties.register() + preferences.register() def unregister(): @@ -50,3 +52,4 @@ def unregister(): gui.unregister() operators.unregister() properties.unregister() + preferences.unregister() diff --git a/scripts/addons_core/viewport_vr_preview/configs/default.py b/scripts/addons_core/viewport_vr_preview/configs/default.py index 3c87aab2e12..3f4c9539ac8 100644 --- a/scripts/addons_core/viewport_vr_preview/configs/default.py +++ b/scripts/addons_core/viewport_vr_preview/configs/default.py @@ -37,7 +37,8 @@ actionconfig_data = \ ("teleport", {"type": 'FLOAT', "user_paths": ['/user/hand/left', '/user/hand/right'], "op": 'wm.xr_navigation_teleport', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("interpolation", 0.9), - ("color", (0.0, 1.0, 1.0, 1.0)), + ("hit_color", (0.0, 1.0, 1.0, 1.0)), + ("miss_color", (1.0, 0.0, 0.0, 1.0)), ], }, {"bindings": @@ -74,6 +75,7 @@ actionconfig_data = \ ("fly_forward", {"type": 'FLOAT', "user_paths": ['/user/hand/left'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'VIEWER_FORWARD'), + ("alt_mode", 'UP'), ("lock_location_z", True), ], }, @@ -92,6 +94,7 @@ actionconfig_data = \ ("fly_back", {"type": 'FLOAT', "user_paths": ['/user/hand/left'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'VIEWER_BACK'), + ("alt_mode", 'DOWN'), ("lock_location_z", True), ], }, @@ -110,6 +113,7 @@ actionconfig_data = \ ("fly_left", {"type": 'FLOAT', "user_paths": ['/user/hand/left'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'VIEWER_LEFT'), + ("alt_mode", 'TURNLEFT'), ("lock_location_z", True), ], }, @@ -128,6 +132,7 @@ actionconfig_data = \ ("fly_right", {"type": 'FLOAT', "user_paths": ['/user/hand/left'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'VIEWER_RIGHT'), + ("alt_mode", 'TURNRIGHT'), ("lock_location_z", True), ], }, @@ -146,8 +151,8 @@ actionconfig_data = \ ("fly_up", {"type": 'FLOAT', "user_paths": ['/user/hand/right'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'UP'), - ("speed_min", 0.014), - ("speed_max", 0.042), + ("alt_mode", 'VIEWER_FORWARD'), + ("alt_lock_location_z", True), ], }, {"bindings": @@ -165,8 +170,8 @@ actionconfig_data = \ ("fly_down", {"type": 'FLOAT', "user_paths": ['/user/hand/right'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'DOWN'), - ("speed_min", 0.014), - ("speed_max", 0.042), + ("alt_mode", 'VIEWER_BACK'), + ("alt_lock_location_z", True), ], }, {"bindings": @@ -184,8 +189,8 @@ actionconfig_data = \ ("fly_turnleft", {"type": 'FLOAT', "user_paths": ['/user/hand/right'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'TURNLEFT'), - ("speed_min", 0.01), - ("speed_max", 0.03), + ("alt_mode", 'VIEWER_LEFT'), + ("alt_lock_location_z", True), ], }, {"bindings": @@ -203,8 +208,8 @@ actionconfig_data = \ ("fly_turnright", {"type": 'FLOAT', "user_paths": ['/user/hand/right'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'TURNRIGHT'), - ("speed_min", 0.01), - ("speed_max", 0.03), + ("alt_mode", 'VIEWER_RIGHT'), + ("alt_lock_location_z", True), ], }, {"bindings": @@ -261,7 +266,8 @@ actionconfig_data = \ {"op_properties": [("interpolation", 0.9), ("from_viewer", True), - ("color", (0.0, 1.0, 1.0, 1.0)), + ("hit_color", (0.0, 1.0, 1.0, 1.0)), + ("miss_color", (1.0, 0.0, 0.0, 1.0)), ], }, {"bindings": @@ -278,6 +284,7 @@ actionconfig_data = \ ("fly_forward", {"type": 'FLOAT', "user_paths": ['/user/gamepad'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'VIEWER_FORWARD'), + ("alt_mode", 'UP'), ("lock_location_z", True), ], }, @@ -289,6 +296,7 @@ actionconfig_data = \ ("fly_back", {"type": 'FLOAT', "user_paths": ['/user/gamepad'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'VIEWER_BACK'), + ("alt_mode", 'DOWN'), ("lock_location_z", True), ], }, @@ -300,6 +308,7 @@ actionconfig_data = \ ("fly_left", {"type": 'FLOAT', "user_paths": ['/user/gamepad'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'VIEWER_LEFT'), + ("alt_mode", 'TURNLEFT'), ("lock_location_z", True), ], }, @@ -311,6 +320,7 @@ actionconfig_data = \ ("fly_right", {"type": 'FLOAT', "user_paths": ['/user/gamepad'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'VIEWER_RIGHT'), + ("alt_mode", 'TURNRIGHT'), ("lock_location_z", True), ], }, @@ -322,9 +332,9 @@ actionconfig_data = \ ("fly_up", {"type": 'FLOAT', "user_paths": ['/user/gamepad'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'UP'), - ("speed_min", 0.014), - ("speed_max", 0.042), - ], + ("alt_mode", 'VIEWER_FORWARD'), + ("alt_lock_location_z", True), + ] }, {"bindings": [("gamepad", {"profile": '/interaction_profiles/microsoft/xbox_controller', "component_paths": ['/input/thumbstick_right/y'], "threshold": '0.30000001192092896', "axis_region": 'POSITIVE'}), @@ -334,9 +344,9 @@ actionconfig_data = \ ("fly_down", {"type": 'FLOAT', "user_paths": ['/user/gamepad'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'DOWN'), - ("speed_min", 0.014), - ("speed_max", 0.042), - ], + ("alt_mode", 'VIEWER_BACK'), + ("alt_lock_location_z", True), + ] }, {"bindings": [("gamepad", {"profile": '/interaction_profiles/microsoft/xbox_controller', "component_paths": ['/input/thumbstick_right/y'], "threshold": '0.30000001192092896', "axis_region": 'NEGATIVE'}), @@ -346,9 +356,9 @@ actionconfig_data = \ ("fly_turnleft", {"type": 'FLOAT', "user_paths": ['/user/gamepad'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'TURNLEFT'), - ("speed_min", 0.01), - ("speed_max", 0.03), - ], + ("alt_mode", 'VIEWER_LEFT'), + ("alt_lock_location_z", True), + ] }, {"bindings": [("gamepad", {"profile": '/interaction_profiles/microsoft/xbox_controller', "component_paths": ['/input/thumbstick_right/x'], "threshold": '0.30000001192092896', "axis_region": 'NEGATIVE'}), @@ -358,9 +368,9 @@ actionconfig_data = \ ("fly_turnright", {"type": 'FLOAT', "user_paths": ['/user/gamepad'], "op": 'wm.xr_navigation_fly', "op_mode": 'MODAL', "bimanual": 'False', "haptic_name": '', "haptic_match_user_paths": 'False', "haptic_duration": '0.0', "haptic_frequency": '0.0', "haptic_amplitude": '0.0', "haptic_mode": 'PRESS'}, {"op_properties": [("mode", 'TURNRIGHT'), - ("speed_min", 0.01), - ("speed_max", 0.03), - ], + ("alt_mode", 'VIEWER_RIGHT'), + ("alt_lock_location_z", True), + ] }, {"bindings": [("gamepad", {"profile": '/interaction_profiles/microsoft/xbox_controller', "component_paths": ['/input/thumbstick_right/x'], "threshold": '0.30000001192092896', "axis_region": 'POSITIVE'}), diff --git a/scripts/addons_core/viewport_vr_preview/defaults.py b/scripts/addons_core/viewport_vr_preview/defaults.py index efbc88eee59..e7487d256be 100644 --- a/scripts/addons_core/viewport_vr_preview/defaults.py +++ b/scripts/addons_core/viewport_vr_preview/defaults.py @@ -37,6 +37,7 @@ class VRDefaultActions(Enum): FLY_TURNLEFT = "fly_turnleft" FLY_TURNRIGHT = "fly_turnright" NAV_RESET = "nav_reset" + SWAP_HANDS = "swap_hands" HAPTIC = "haptic" HAPTIC_LEFT = "haptic_left" HAPTIC_RIGHT = "haptic_right" @@ -1146,6 +1147,91 @@ def vr_defaults_create_default(session_state): 'ANY', 'ANY') + ami = vr_defaults_action_add(am, + VRDefaultActions.SWAP_HANDS.value, + ["/user/hand/left", + "/user/hand/right"], + "wm.xr_navigation_swap_hands", + 'PRESS', + False, + "haptic", + True, + 0.3, + 3000.0, + 0.5, + 'PRESS') + if ami: + """ + vr_defaults_actionbinding_add(ami, + VRDefaultActionbindings.HUAWEI.value, + VRDefaultActionprofiles.HUAWEI.value, + ["/input/back/click", + "/input/back/click"], + 0.3, + 'ANY', + 'ANY') + """ + vr_defaults_actionbinding_add(ami, + VRDefaultActionbindings.INDEX.value, + VRDefaultActionprofiles.INDEX.value, + ["/input/b/click", + "/input/b/click"], + 0.3, + 'ANY', + 'ANY') + vr_defaults_actionbinding_add(ami, + VRDefaultActionbindings.OCULUS.value, + VRDefaultActionprofiles.OCULUS.value, + ["/input/y/click", + "/input/b/click"], + 0.3, + 'ANY', + 'ANY') + vr_defaults_actionbinding_add(ami, + VRDefaultActionbindings.REVERB_G2.value, + VRDefaultActionprofiles.REVERB_G2.value, + ["/input/y/click", + "/input/b/click"], + 0.3, + 'ANY', + 'ANY') + """ + vr_defaults_actionbinding_add(ami, + VRDefaultActionbindings.VIVE.value, + VRDefaultActionprofiles.VIVE.value, + ["/input/menu/click", + "/input/menu/click"], + 0.3, + 'ANY', + 'ANY') + """ + vr_defaults_actionbinding_add(ami, + VRDefaultActionbindings.VIVE_COSMOS.value, + VRDefaultActionprofiles.VIVE_COSMOS.value, + ["/input/y/click", + "/input/b/click"], + 0.3, + 'ANY', + 'ANY') + vr_defaults_actionbinding_add(ami, + VRDefaultActionbindings.VIVE_FOCUS.value, + VRDefaultActionprofiles.VIVE_FOCUS.value, + ["/input/y/click", + "/input/b/click"], + 0.3, + 'ANY', + 'ANY') + """ + vr_defaults_actionbinding_add(ami, + VRDefaultActionbindings.WMR.value, + VRDefaultActionprofiles.WMR.value, + ["/input/menu/click", + "/input/menu/click"], + 0.3, + 'ANY', + 'ANY') + """ + ami = vr_defaults_haptic_action_add(am, VRDefaultActions.HAPTIC.value, ["/user/hand/left", diff --git a/scripts/addons_core/viewport_vr_preview/gui.py b/scripts/addons_core/viewport_vr_preview/gui.py index 4116f2622a6..b873d5f0bc7 100644 --- a/scripts/addons_core/viewport_vr_preview/gui.py +++ b/scripts/addons_core/viewport_vr_preview/gui.py @@ -97,7 +97,9 @@ class VIEW3D_PT_vr_session_view(Panel): col = layout.column(align=True) col.prop(session_settings, "clip_start", text="Clip Start") col.prop(session_settings, "clip_end", text="End", text_ctxt=i18n_contexts.id_camera) - + + col = layout.column(align=True) + col.prop(session_settings, "fly_speed", text="Fly Speed") class VIEW3D_PT_vr_session_view_object_type_visibility(VIEW3D_PT_object_type_visibility): def draw(self, context): diff --git a/scripts/addons_core/viewport_vr_preview/preferences.py b/scripts/addons_core/viewport_vr_preview/preferences.py new file mode 100644 index 00000000000..384bb11acd8 --- /dev/null +++ b/scripts/addons_core/viewport_vr_preview/preferences.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2023 Blender Foundation +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import bpy +from bpy.types import ( + Panel, +) + +class USERPREF_PT_vr_navigation(Panel): + bl_space_type = 'PREFERENCES' + bl_region_type = 'WINDOW' + bl_context = "navigation" + bl_label = "VR Navigation" + + def draw(self, context): + layout = self.layout + width = context.region.width + ui_scale = context.preferences.system.ui_scale + # No horizontal margin if region is rather small. + is_wide = width > (350 * ui_scale) + + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + row = layout.row() + if is_wide: + row.label() # Needed so col below is centered. + + col = row.column() + col.ui_units_x = 50 + + # Implemented by sub-classes. + self.draw_centered(context, col) + + if is_wide: + row.label() # Needed so col above is centered. + + def draw_centered(self, context, layout): + prefs = context.preferences + nav = prefs.inputs.xr_navigation + + col = layout.column() + + col.row().prop(nav, "vignette_intensity", text="Vignette Intensity") + + if nav.snap_turn: + col.row().prop(nav, "turn_amount", text="Turn Amount") + else: + col.row().prop(nav, "turn_speed", text="Turn Speed") + + col.row().prop(nav, "snap_turn", text="Snap Turn") + col.row().prop(nav, "invert_rotation", text="Invert Rotation") + +classes = ( + USERPREF_PT_vr_navigation, +) + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + +def unregister(): + from bpy.utils import unregister_class + for cls in classes: + unregister_class(cls) diff --git a/source/blender/blenkernel/BKE_blender_version.h b/source/blender/blenkernel/BKE_blender_version.h index 11d16c92eaf..7b810a88d15 100644 --- a/source/blender/blenkernel/BKE_blender_version.h +++ b/source/blender/blenkernel/BKE_blender_version.h @@ -27,7 +27,7 @@ /* Blender file format version. */ #define BLENDER_FILE_VERSION BLENDER_VERSION -#define BLENDER_FILE_SUBVERSION 98 +#define BLENDER_FILE_SUBVERSION 99 /* Minimum Blender version that supports reading file written with the current * version. Older Blender versions will test this and cancel loading the file, showing a warning to diff --git a/source/blender/blenloader/intern/versioning_500.cc b/source/blender/blenloader/intern/versioning_500.cc index 19add376882..41c9bbc0e73 100644 --- a/source/blender/blenloader/intern/versioning_500.cc +++ b/source/blender/blenloader/intern/versioning_500.cc @@ -3772,6 +3772,12 @@ void blo_do_versions_500(FileData *fd, Library * /*lib*/, Main *bmain) } } + if (!MAIN_VERSION_FILE_ATLEAST(bmain, 500, 99)) { + LISTBASE_FOREACH (wmWindowManager *, wm, &bmain->wm) { + wm->xr.session_settings.fly_speed = 3.0f; + } + } + /** * Always bump subversion in BKE_blender_version.h when adding versioning * code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check. diff --git a/source/blender/blenloader/intern/versioning_defaults.cc b/source/blender/blenloader/intern/versioning_defaults.cc index e28db2f9736..02a65b6c6fc 100644 --- a/source/blender/blenloader/intern/versioning_defaults.cc +++ b/source/blender/blenloader/intern/versioning_defaults.cc @@ -381,6 +381,11 @@ static void blo_update_defaults_paint(Paint *paint) } } +static void blo_update_defaults_windowmanager(wmWindowManager *wm) +{ + wm->xr.session_settings.fly_speed = 3.0f; +} + static void blo_update_defaults_scene(Main *bmain, Scene *scene) { ToolSettings *ts = scene->toolsettings; @@ -618,6 +623,8 @@ void BLO_update_defaults_startup_blend(Main *bmain, const char *app_template) /* Work-spaces. */ LISTBASE_FOREACH (wmWindowManager *, wm, &bmain->wm) { + blo_update_defaults_windowmanager(wm); + LISTBASE_FOREACH (wmWindow *, win, &wm->windows) { LISTBASE_FOREACH (WorkSpace *, workspace, &bmain->workspaces) { WorkSpaceLayout *layout = BKE_workspace_active_layout_for_workspace_get( diff --git a/source/blender/blenloader/intern/versioning_userdef.cc b/source/blender/blenloader/intern/versioning_userdef.cc index 52cb528ebe0..6363ff00d16 100644 --- a/source/blender/blenloader/intern/versioning_userdef.cc +++ b/source/blender/blenloader/intern/versioning_userdef.cc @@ -1691,6 +1691,13 @@ void blo_do_versions_userdef(UserDef *userdef) } } + if (!USER_VERSION_ATLEAST(500, 99)) { + userdef->xr_navigation.vignette_intensity = 50.0f; + userdef->xr_navigation.turn_amount = DEG2RAD(30); + userdef->xr_navigation.turn_speed = DEG2RAD(60); + userdef->xr_navigation.flag = USER_XR_NAV_SNAP_TURN; + } + /** * Always bump subversion in BKE_blender_version.h when adding versioning * code here, and wrap it inside a USER_VERSION_ATLEAST check. diff --git a/source/blender/draw/engines/overlay/overlay_background.hh b/source/blender/draw/engines/overlay/overlay_background.hh index ed0caf58929..f9ce28ad0c7 100644 --- a/source/blender/draw/engines/overlay/overlay_background.hh +++ b/source/blender/draw/engines/overlay/overlay_background.hh @@ -22,6 +22,7 @@ namespace blender::draw::overlay { class Background : Overlay { private: PassSimple bg_ps_ = {"Background"}; + PassSimple bg_vignette_ps_ = {"Background Vignette"}; gpu::FrameBuffer *framebuffer_ref_ = nullptr; @@ -31,6 +32,7 @@ class Background : Overlay { DRWState pass_state = DRW_STATE_WRITE_COLOR | DRW_STATE_BLEND_BACKGROUND; float4 color_override(0.0f, 0.0f, 0.0f, 0.0f); int background_type; + const float vignette_aperture = state.v3d->vignette_aperture, vignette_falloff = 0.15f; if (state.is_viewport_image_render && !state.draw_background) { background_type = BG_SOLID; @@ -96,7 +98,26 @@ class Background : Overlay { bg_ps_.bind_texture("depth_buffer", &res.depth_tx); bg_ps_.push_constant("color_override", color_override); bg_ps_.push_constant("bg_type", background_type); + bg_ps_.push_constant("vignette_enabled", false); bg_ps_.draw_procedural(GPU_PRIM_TRIS, 1, 3); + + if (state.vignette_enabled) { + bg_vignette_ps_.init(); + bg_vignette_ps_.framebuffer_set(&framebuffer_ref_); + + bg_vignette_ps_.state_set(DRW_STATE_WRITE_COLOR | DRW_STATE_BLEND_ALPHA); + bg_vignette_ps_.shader_set(res.shaders->background_fill.get()); + bg_vignette_ps_.bind_ubo(OVERLAY_GLOBALS_SLOT, &res.globals_buf); + bg_vignette_ps_.bind_ubo(DRW_CLIPPING_UBO_SLOT, &res.clip_planes_buf); + bg_vignette_ps_.bind_texture("color_buffer", &res.color_render_tx); + bg_vignette_ps_.bind_texture("depth_buffer", &res.depth_tx); + bg_vignette_ps_.push_constant("color_override", color_override); + bg_vignette_ps_.push_constant("bg_type", background_type); + bg_vignette_ps_.push_constant("vignette_enabled", true); + bg_vignette_ps_.push_constant("vignette_aperture", vignette_aperture); + bg_vignette_ps_.push_constant("vignette_falloff", vignette_falloff); + bg_vignette_ps_.draw_procedural(GPU_PRIM_TRIS, 1, 3); + } } void draw_output(Framebuffer &framebuffer, Manager &manager, View &view) final @@ -104,6 +125,12 @@ class Background : Overlay { framebuffer_ref_ = framebuffer; manager.submit(bg_ps_, view); } + + void draw_vignette(Framebuffer &framebuffer, Manager &manager, View &view) + { + framebuffer_ref_ = framebuffer; + manager.submit(bg_vignette_ps_, view); + } }; } // namespace blender::draw::overlay diff --git a/source/blender/draw/engines/overlay/overlay_instance.cc b/source/blender/draw/engines/overlay/overlay_instance.cc index 15936017030..0c79c8d3fa4 100644 --- a/source/blender/draw/engines/overlay/overlay_instance.cc +++ b/source/blender/draw/engines/overlay/overlay_instance.cc @@ -66,6 +66,8 @@ void Instance::init() state.xray_opacity = state.xray_enabled ? XRAY_ALPHA(state.v3d) : 1.0f; state.xray_flag_enabled = SHADING_XRAY_FLAG_ENABLED(state.v3d->shading) && !state.is_depth_only_drawing; + state.vignette_enabled = ctx->mode == DRWContext::VIEWPORT_XR && + state.v3d->vignette_aperture < M_SQRT1_2; const bool viewport_uses_workbench = state.v3d->shading.type <= OB_SOLID || BKE_scene_uses_blender_workbench(state.scene); @@ -962,6 +964,10 @@ void Instance::draw_v3d(Manager &manager, View &view) cursor.draw_output(resources.overlay_output_color_only_fb, manager, view); draw_text(resources.overlay_output_color_only_fb); + + if (state.vignette_enabled) { + background.draw_vignette(resources.overlay_output_color_only_fb, manager, view); + } } } diff --git a/source/blender/draw/engines/overlay/overlay_private.hh b/source/blender/draw/engines/overlay/overlay_private.hh index 411f4d32ddb..931357539eb 100644 --- a/source/blender/draw/engines/overlay/overlay_private.hh +++ b/source/blender/draw/engines/overlay/overlay_private.hh @@ -153,6 +153,8 @@ struct State { bool draw_background = false; /** True if the render engine outputs satisfactory depth information to the depth buffer. */ bool is_render_depth_available = false; + /** Whether we should render a vignette over the scene. */ + bool vignette_enabled = false; /** Should text draw in this mode? */ bool show_text = false; bool hide_overlays = false; diff --git a/source/blender/draw/engines/overlay/shaders/infos/overlay_background_infos.hh b/source/blender/draw/engines/overlay/shaders/infos/overlay_background_infos.hh index 427f52e30f6..ec33ca7a59c 100644 --- a/source/blender/draw/engines/overlay/shaders/infos/overlay_background_infos.hh +++ b/source/blender/draw/engines/overlay/shaders/infos/overlay_background_infos.hh @@ -22,6 +22,9 @@ SAMPLER(0, sampler2D, color_buffer) SAMPLER(1, sampler2DDepth, depth_buffer) PUSH_CONSTANT(int, bg_type) PUSH_CONSTANT(float4, color_override) +PUSH_CONSTANT(float, vignette_aperture) +PUSH_CONSTANT(float, vignette_falloff) +PUSH_CONSTANT(bool, vignette_enabled) FRAGMENT_SOURCE("overlay_background_frag.glsl") FRAGMENT_OUT(0, float4, frag_color) ADDITIONAL_INFO(gpu_fullscreen) diff --git a/source/blender/draw/engines/overlay/shaders/overlay_background_frag.glsl b/source/blender/draw/engines/overlay/shaders/overlay_background_frag.glsl index a0c13ccd897..e3c939d6d1c 100644 --- a/source/blender/draw/engines/overlay/shaders/overlay_background_frag.glsl +++ b/source/blender/draw/engines/overlay/shaders/overlay_background_frag.glsl @@ -31,8 +31,19 @@ void main() * This removes the alpha channel and put the background behind reference images * while masking the reference images by the render alpha. */ - float alpha = texture(color_buffer, screen_uv).a; - float depth = texture(depth_buffer, screen_uv).r; + + float alpha; + float depth; + + if (vignette_enabled) { + const float dist = length(screen_uv - 0.5f); + alpha = smoothstep(vignette_aperture, vignette_aperture + vignette_falloff, dist); + depth = 0.0f; + } + else { + alpha = texture(color_buffer, screen_uv).a; + depth = texture(depth_buffer, screen_uv).r; + } float3 bg_col; float3 col_high; @@ -90,11 +101,16 @@ void main() bg_col = mix(bg_col, color_override.rgb, color_override.a); - /* Mimic alpha under behavior. Result is premultiplied. */ - frag_color = float4(bg_col, 1.0f) * (1.0f - alpha); + if (vignette_enabled) { + frag_color = float4(bg_col, alpha); + } + else { + /* Mimic alpha under behavior. Result is premultiplied. */ + frag_color = float4(bg_col, 1.0f) * (1.0f - alpha); - /* Special case: If the render is not transparent, do not clear alpha values. */ - if (depth == 1.0f && alpha == 1.0f) { - frag_color.a = 1.0f; + /* Special case: If the render is not transparent, do not clear alpha values. */ + if (depth == 1.0f && alpha == 1.0f) { + frag_color.a = 1.0f; + } } } diff --git a/source/blender/editors/include/ED_view3d_offscreen.hh b/source/blender/editors/include/ED_view3d_offscreen.hh index f46943c6bd3..13e4778a453 100644 --- a/source/blender/editors/include/ED_view3d_offscreen.hh +++ b/source/blender/editors/include/ED_view3d_offscreen.hh @@ -55,6 +55,7 @@ void ED_view3d_draw_offscreen_simple(Depsgraph *depsgraph, const float winmat[4][4], float clip_start, float clip_end, + float vignette_aperture, bool is_xr_surface, bool is_image_render, bool draw_background, diff --git a/source/blender/editors/space_view3d/view3d_draw.cc b/source/blender/editors/space_view3d/view3d_draw.cc index 5b086b9b0c8..b0d14a5b39a 100644 --- a/source/blender/editors/space_view3d/view3d_draw.cc +++ b/source/blender/editors/space_view3d/view3d_draw.cc @@ -1884,6 +1884,7 @@ void ED_view3d_draw_offscreen_simple(Depsgraph *depsgraph, const float winmat[4][4], float clip_start, float clip_end, + float vignette_aperture, bool is_xr_surface, bool is_image_render, bool draw_background, @@ -1965,6 +1966,7 @@ void ED_view3d_draw_offscreen_simple(Depsgraph *depsgraph, v3d.clip_end = clip_end; /* Actually not used since we pass in the projection matrix. */ v3d.lens = 0; + v3d.vignette_aperture = vignette_aperture; /* WORKAROUND: Disable overscan because it is not supported for arbitrary input matrices. * The proper fix to this would be to support arbitrary matrices in `eevee::Camera::sync()`. */ diff --git a/source/blender/gpu/CMakeLists.txt b/source/blender/gpu/CMakeLists.txt index dbf457075a7..3a271fb6669 100644 --- a/source/blender/gpu/CMakeLists.txt +++ b/source/blender/gpu/CMakeLists.txt @@ -486,6 +486,7 @@ set(GLSL_SRC shaders/infos/gpu_shader_sequencer_infos.hh shaders/infos/gpu_shader_simple_lighting_infos.hh shaders/infos/gpu_shader_text_infos.hh + shaders/infos/gpu_shader_xr_raycast_infos.hh shaders/infos/gpu_srgb_to_framebuffer_space_infos.hh shaders/gpu_shader_depth_only_frag.glsl @@ -540,6 +541,7 @@ set(GLSL_SRC shaders/gpu_shader_text_vert.glsl shaders/gpu_shader_text_frag.glsl + shaders/gpu_shader_xr_raycast_vert.glsl shaders/gpu_shader_keyframe_shape_vert.glsl shaders/gpu_shader_keyframe_shape_frag.glsl diff --git a/source/blender/gpu/GPU_shader_builtin.hh b/source/blender/gpu/GPU_shader_builtin.hh index e68e5dd5868..4c379f88b00 100644 --- a/source/blender/gpu/GPU_shader_builtin.hh +++ b/source/blender/gpu/GPU_shader_builtin.hh @@ -91,6 +91,9 @@ enum GPUBuiltinShader { /** Draw sequencer zebra pattern (overexposed regions). */ GPU_SHADER_SEQUENCER_ZEBRA, + /** Draw xr raycast as a ruled spline surface. */ + GPU_SHADER_XR_RAYCAST, + /** Compute shaders to generate 2d index buffers (mainly for curve drawing). */ GPU_SHADER_INDEXBUF_POINTS, GPU_SHADER_INDEXBUF_LINES, diff --git a/source/blender/gpu/GPU_xr_defines.hh b/source/blender/gpu/GPU_xr_defines.hh new file mode 100644 index 00000000000..62ad68751a0 --- /dev/null +++ b/source/blender/gpu/GPU_xr_defines.hh @@ -0,0 +1,11 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup gpu + */ + +#pragma once + +#define XR_MAX_RAYCASTS 8 diff --git a/source/blender/gpu/intern/gpu_shader_builtin.cc b/source/blender/gpu/intern/gpu_shader_builtin.cc index 5af0826d686..15c420a258d 100644 --- a/source/blender/gpu/intern/gpu_shader_builtin.cc +++ b/source/blender/gpu/intern/gpu_shader_builtin.cc @@ -119,6 +119,8 @@ static const char *builtin_shader_create_info_name(GPUBuiltinShader shader) return "gpu_shader_index_2d_array_lines"; case GPU_SHADER_INDEXBUF_TRIS: return "gpu_shader_index_2d_array_tris"; + case GPU_SHADER_XR_RAYCAST: + return "gpu_shader_xr_raycast"; default: BLI_assert_unreachable(); return ""; diff --git a/source/blender/gpu/shaders/gpu_shader_xr_raycast_vert.glsl b/source/blender/gpu/shaders/gpu_shader_xr_raycast_vert.glsl new file mode 100644 index 00000000000..23807079066 --- /dev/null +++ b/source/blender/gpu/shaders/gpu_shader_xr_raycast_vert.glsl @@ -0,0 +1,43 @@ +/* SPDX-FileCopyrightText: 2016-2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "infos/gpu_shader_xr_raycast_infos.hh" + +VERTEX_SHADER_CREATE_INFO(gpu_shader_xr_raycast) + +vec3 catmull_rom(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t) +{ + float t2 = t * t; + float t3 = t2 * t; + return 0.5 * ((2.0 * p1) + (-p0 + p2) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3); +} + +vec3 get_control_point(int idx) +{ + idx = clamp(idx, 0, control_point_count - 1); + return control_points[idx].xyz; +} + +void main() +{ + int sample_idx = gl_VertexID >> 1; + float side = ((gl_VertexID & 1) != 0) ? -1.0 : 1.0; + + /** Interpolate within the range: [0, segment_count] */ + float sample_value = float(sample_idx) * float(control_point_count - 1) / + float(sample_count - 1); + + int segment_idx = int(sample_value); + float t = sample_value - float(segment_idx); + + vec3 p0 = get_control_point(segment_idx - 1); + vec3 p1 = get_control_point(segment_idx + 0); + vec3 p2 = get_control_point(segment_idx + 1); + vec3 p3 = get_control_point(segment_idx + 2); + + vec3 pos = catmull_rom(p0, p1, p2, p3, t) + 0.5 * width * side * right_vector; + + gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0); +} diff --git a/source/blender/gpu/shaders/infos/gpu_shader_xr_raycast_infos.hh b/source/blender/gpu/shaders/infos/gpu_shader_xr_raycast_infos.hh new file mode 100644 index 00000000000..977ffd40b68 --- /dev/null +++ b/source/blender/gpu/shaders/infos/gpu_shader_xr_raycast_infos.hh @@ -0,0 +1,33 @@ +/* SPDX-FileCopyrightText: 2022 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup gpu + */ + +#ifdef GPU_SHADER +# pragma once +# include "gpu_glsl_cpp_stubs.hh" + +# include "GPU_shader_shared.hh" +#endif + +#include "GPU_xr_defines.hh" +#include "gpu_shader_create_info.hh" + +GPU_SHADER_CREATE_INFO(gpu_shader_xr_raycast) +DEFINE_VALUE("XR_MAX_RAYCASTS", STRINGIFY(XR_MAX_RAYCASTS)) +FRAGMENT_OUT(0, float4, fragColor) +PUSH_CONSTANT_ARRAY(float4, control_points, XR_MAX_RAYCASTS + 1) +PUSH_CONSTANT(float4x4, ModelViewProjectionMatrix) +PUSH_CONSTANT(float4, color) +PUSH_CONSTANT(float3, right_vector) +PUSH_CONSTANT(float, width) +PUSH_CONSTANT(int, control_point_count) +PUSH_CONSTANT(int, sample_count) +VERTEX_SOURCE("gpu_shader_xr_raycast_vert.glsl") +FRAGMENT_SOURCE("gpu_shader_uniform_color_frag.glsl") +ADDITIONAL_INFO(gpu_srgb_to_framebuffer_space) +DO_STATIC_COMPILATION() +GPU_SHADER_CREATE_END() diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index ec1092ad752..79cb4b928ad 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -167,6 +167,14 @@ typedef struct WalkNavigation { char _pad0[6]; } WalkNavigation; +typedef struct XrNavigation { + float vignette_intensity; + float turn_speed; + float turn_amount; + short flag; + char _pad0[2]; +} XrNavigation; + typedef struct UserDef_Runtime { /** Mark as changed so the preferences are saved on exit. */ char is_dirty; @@ -619,6 +627,7 @@ typedef struct UserDef { char statusbar_flag; /* eUserpref_StatusBar_Flag */ struct WalkNavigation walk_navigation; + struct XrNavigation xr_navigation; /** The UI for the user preferences. */ UserDef_SpaceData space_data; @@ -1108,6 +1117,12 @@ typedef enum eUserpref_FactorDisplay { USER_FACTOR_AS_PERCENTAGE = 1, } eUserpref_FactorDisplay; +/** #UserDef.xr_navigation_flag */ +typedef enum eUserpref_XrNavigationFlags { + USER_XR_NAV_SNAP_TURN = (1 << 0), + USER_XR_NAV_INVERT_ROTATION = (1 << 1), +} eUserpref_XrNavigationFlags; + typedef enum eUserpref_RenderDisplayType { USER_RENDER_DISPLAY_NONE = 0, USER_RENDER_DISPLAY_SCREEN = 1, diff --git a/source/blender/makesdna/DNA_view3d_types.h b/source/blender/makesdna/DNA_view3d_types.h index d70575ade76..6c54c560ddf 100644 --- a/source/blender/makesdna/DNA_view3d_types.h +++ b/source/blender/makesdna/DNA_view3d_types.h @@ -350,7 +350,8 @@ typedef struct View3D { float lens, grid; float clip_start, clip_end; - float ofs[3] DNA_DEPRECATED; + float vignette_aperture; + float ofs[2] DNA_DEPRECATED; char _pad[1]; diff --git a/source/blender/makesdna/DNA_xr_types.h b/source/blender/makesdna/DNA_xr_types.h index 7466ebc8296..ba2cc62ccd1 100644 --- a/source/blender/makesdna/DNA_xr_types.h +++ b/source/blender/makesdna/DNA_xr_types.h @@ -38,6 +38,10 @@ typedef struct XrSessionSettings { /** Object type settings to apply to VR view (unlike shading, not shared with window 3D-View). */ int object_type_exclude_viewport; int object_type_exclude_select; + + /** Fly speed. */ + float fly_speed; + float padding; } XrSessionSettings; typedef enum eXrSessionFlag { diff --git a/source/blender/makesdna/intern/dna_defaults.c b/source/blender/makesdna/intern/dna_defaults.c index 4443a6df0fd..f4ba83d7523 100644 --- a/source/blender/makesdna/intern/dna_defaults.c +++ b/source/blender/makesdna/intern/dna_defaults.c @@ -531,6 +531,7 @@ const void *DNA_default_table[SDNA_TYPE_MAX] = { SDNA_DEFAULT_DECL_EX(UserDef_SpaceData, UserDef.space_data), SDNA_DEFAULT_DECL_EX(UserDef_FileSpaceData, UserDef.file_space_data), SDNA_DEFAULT_DECL_EX(WalkNavigation, UserDef.walk_navigation), + SDNA_DEFAULT_DECL_EX(XrNavigation, UserDef.xr_navigation), SDNA_DEFAULT_DECL(bUserAssetLibrary), SDNA_DEFAULT_DECL(bUserExtensionRepo), SDNA_DEFAULT_DECL(bUserAssetShelfSettings), diff --git a/source/blender/makesrna/intern/rna_userdef.cc b/source/blender/makesrna/intern/rna_userdef.cc index f5fe86e4d24..d010c6045fb 100644 --- a/source/blender/makesrna/intern/rna_userdef.cc +++ b/source/blender/makesrna/intern/rna_userdef.cc @@ -4754,6 +4754,43 @@ static void rna_def_userdef_walk_navigation(BlenderRNA *brna) RNA_def_property_ui_text(prop, "Reverse Mouse", "Reverse the vertical movement of the mouse"); } +static void rna_def_userdef_xr_navigation(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "XrNavigation", nullptr); + RNA_def_struct_sdna(srna, "XrNavigation"); + RNA_def_struct_ui_text(srna, "VR Navigation", "VR navigation settings"); + + prop = RNA_def_property(srna, "vignette_intensity", PROP_FLOAT, PROP_PERCENTAGE); + RNA_def_property_range(prop, 0, 100.0); + RNA_def_property_ui_range(prop, 0, 100.0, 1000, 0); + RNA_def_property_ui_text( + prop, "Vignette Intensity", "Intensity of vignette that appears when moving"); + + prop = RNA_def_property(srna, "turn_speed", PROP_FLOAT, PROP_ANGLE); + RNA_def_property_range(prop, 0, FLT_MAX); + RNA_def_property_ui_range(prop, 0, FLT_MAX, 1000, 0); + RNA_def_property_ui_text(prop, "Turn Speed", "Turn speed in degrees per second"); + + prop = RNA_def_property(srna, "turn_amount", PROP_FLOAT, PROP_ANGLE); + RNA_def_property_range(prop, 0, DEG2RAD(360)); + RNA_def_property_ui_range(prop, 0, DEG2RAD(360), 1000, 0); + RNA_def_property_ui_text(prop, "Turn Amount", "Amount in degrees per turn when using snap turn"); + + prop = RNA_def_property(srna, "snap_turn", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flag", USER_XR_NAV_SNAP_TURN); + RNA_def_property_ui_text( + prop, + "Snap Turn", + "Instantly rotates the camera by a fixed angle instead of smoothly turning"); + + prop = RNA_def_property(srna, "invert_rotation", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flag", USER_XR_NAV_INVERT_ROTATION); + RNA_def_property_ui_text(prop, "Invert Rotation", "Reverses the direction of rotation input"); +} + static void rna_def_userdef_view(BlenderRNA *brna) { static const EnumPropertyItem timecode_styles[] = { @@ -6493,6 +6530,12 @@ static void rna_def_userdef_input(BlenderRNA *brna) "restarting Blender for changes to take effect)"); RNA_def_property_update(prop, 0, "rna_userdef_input_devices"); + prop = RNA_def_property(srna, "xr_navigation", PROP_POINTER, PROP_NONE); + RNA_def_property_pointer_sdna(prop, nullptr, "xr_navigation"); + RNA_def_property_flag(prop, PROP_NEVER_NULL); + RNA_def_property_struct_type(prop, "XrNavigation"); + RNA_def_property_ui_text(prop, "XR Navigation", "Settings for navigation in XR"); + # ifdef WITH_INPUT_NDOF /* 3D mouse settings */ /* global options */ @@ -7439,6 +7482,7 @@ void RNA_def_userdef(BlenderRNA *brna) rna_def_userdef_dothemes(brna); rna_def_userdef_solidlight(brna); rna_def_userdef_walk_navigation(brna); + rna_def_userdef_xr_navigation(brna); srna = RNA_def_struct(brna, "Preferences", nullptr); RNA_def_struct_sdna(srna, "UserDef"); diff --git a/source/blender/makesrna/intern/rna_xr.cc b/source/blender/makesrna/intern/rna_xr.cc index 21b65bade5a..27839f8d04f 100644 --- a/source/blender/makesrna/intern/rna_xr.cc +++ b/source/blender/makesrna/intern/rna_xr.cc @@ -2035,16 +2035,22 @@ static void rna_def_xr_session_settings(BlenderRNA *brna) prop = RNA_def_property(srna, "clip_start", PROP_FLOAT, PROP_DISTANCE); RNA_def_property_range(prop, 1e-6f, FLT_MAX); - RNA_def_property_ui_range(prop, 0.001f, FLT_MAX, 10, 3); + RNA_def_property_ui_range(prop, 0.001f, FLT_MAX, 0.1 * 100, 3); RNA_def_property_ui_text(prop, "Clip Start", "VR viewport near clipping distance"); RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr); prop = RNA_def_property(srna, "clip_end", PROP_FLOAT, PROP_DISTANCE); RNA_def_property_range(prop, 1e-6f, FLT_MAX); - RNA_def_property_ui_range(prop, 0.001f, FLT_MAX, 10, 3); + RNA_def_property_ui_range(prop, 0.001f, FLT_MAX, 10 * 100, 3); RNA_def_property_ui_text(prop, "Clip End", "VR viewport far clipping distance"); RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr); + prop = RNA_def_property(srna, "fly_speed", PROP_FLOAT, PROP_NONE); + RNA_def_property_range(prop, 1e-6f, FLT_MAX); + RNA_def_property_ui_range(prop, 0.001f, FLT_MAX, 0.5 * 100, 3); + RNA_def_property_ui_text(prop, "Fly Speed", "Fly speed in meters per second"); + RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr); + prop = RNA_def_property(srna, "use_positional_tracking", PROP_BOOLEAN, PROP_NONE); RNA_def_property_boolean_funcs(prop, "rna_XrSessionSettings_use_positional_tracking_get", diff --git a/source/blender/windowmanager/CMakeLists.txt b/source/blender/windowmanager/CMakeLists.txt index 703ae9f2ed4..a8bbd992ab1 100644 --- a/source/blender/windowmanager/CMakeLists.txt +++ b/source/blender/windowmanager/CMakeLists.txt @@ -8,6 +8,7 @@ set(INC gizmo/intern ../compositor ../editors/include + ../gpu ../makesrna ../../../intern/memutil diff --git a/source/blender/windowmanager/WM_api.hh b/source/blender/windowmanager/WM_api.hh index de6f85b5d73..e94674330d2 100644 --- a/source/blender/windowmanager/WM_api.hh +++ b/source/blender/windowmanager/WM_api.hh @@ -2180,6 +2180,9 @@ void WM_xr_session_state_nav_rotation_set(wmXrData *xr, const float rotation[4]) bool WM_xr_session_state_nav_scale_get(const wmXrData *xr, float *r_scale); void WM_xr_session_state_nav_scale_set(wmXrData *xr, float scale); void WM_xr_session_state_navigation_reset(wmXrSessionState *state); +void WM_xr_session_state_vignette_reset(wmXrSessionState *state); +void WM_xr_session_state_vignette_activate(wmXrData *xr); +void WM_xr_session_state_vignette_update(wmXrSessionState *state); ARegionType *WM_xr_surface_controller_region_type_get(); diff --git a/source/blender/windowmanager/xr/intern/wm_xr.cc b/source/blender/windowmanager/xr/intern/wm_xr.cc index c8936682af6..17f5a06c666 100644 --- a/source/blender/windowmanager/xr/intern/wm_xr.cc +++ b/source/blender/windowmanager/xr/intern/wm_xr.cc @@ -184,6 +184,8 @@ void wm_xr_runtime_data_free(wmXrRuntimeData **runtime) if ((*runtime)->area) { wmWindowManager *wm = static_cast(G_MAIN->wm.first); wmWindow *win = wm_xr_session_root_window_or_fallback_get(wm, (*runtime)); + + WM_event_remove_handlers_by_area(&win->handlers, (*runtime)->area); ED_area_offscreen_free(wm, win, (*runtime)->area); (*runtime)->area = nullptr; } diff --git a/source/blender/windowmanager/xr/intern/wm_xr_draw.cc b/source/blender/windowmanager/xr/intern/wm_xr_draw.cc index ca4d5c849f2..0f77254eb53 100644 --- a/source/blender/windowmanager/xr/intern/wm_xr_draw.cc +++ b/source/blender/windowmanager/xr/intern/wm_xr_draw.cc @@ -30,6 +30,8 @@ #include "GPU_state.hh" #include "GPU_viewport.hh" +#include "UI_resources.hh" + #include "WM_api.hh" #include "wm_xr_intern.hh" @@ -180,6 +182,7 @@ void wm_xr_draw_view(const GHOST_XrDrawViewInfo *draw_view, void *customdata) winmat, settings->clip_start, settings->clip_end, + session_state->vignette_data->aperture, true, false, true, @@ -337,7 +340,7 @@ static void wm_xr_controller_aim_draw(const XrSessionSettings *settings, wmXrSes break; case XR_CONTROLLER_DRAW_DARK_RAY: case XR_CONTROLLER_DRAW_LIGHT_RAY: - draw_ray = true; + draw_ray = !state->is_raycast_shown; break; } diff --git a/source/blender/windowmanager/xr/intern/wm_xr_intern.hh b/source/blender/windowmanager/xr/intern/wm_xr_intern.hh index d2391e2cd5e..54f2096fa44 100644 --- a/source/blender/windowmanager/xr/intern/wm_xr_intern.hh +++ b/source/blender/windowmanager/xr/intern/wm_xr_intern.hh @@ -53,6 +53,8 @@ struct wmXrSessionState { bool force_reset_to_base_pose; bool is_view_data_set; + bool swap_hands; + bool is_raycast_shown; /** Current navigation transforms. */ GHOST_XrPose nav_pose; @@ -72,6 +74,12 @@ struct wmXrSessionState { struct wmXrActionSet *active_action_set; /* Name of the action set (if any) to activate before the next actions sync. */ char active_action_set_next[64]; /* #MAX_NAME. */ + + /** The current state and parameters of the vignette that appears while moving. */ + struct wmXrVignetteData *vignette_data; + + /** Model used to draw teleportation raycast. */ + blender::gpu::Batch *raycast_model; }; struct wmXrRuntimeData { @@ -200,6 +208,22 @@ struct wmXrActionSet { ListBase active_haptic_actions; }; +struct wmXrVignetteData { + /** Vignette state. */ + float aperture; + float aperture_velocity; + + /** Vignette parameters. */ + float initial_aperture; + float initial_aperture_velocity; + + float aperture_min; + float aperture_max; + + float aperture_velocity_max; + float aperture_velocity_delta; +}; + /* `wm_xr.cc` */ wmXrRuntimeData *wm_xr_runtime_data_create(); diff --git a/source/blender/windowmanager/xr/intern/wm_xr_operators.cc b/source/blender/windowmanager/xr/intern/wm_xr_operators.cc index 2cc8da7f30a..36dce78b686 100644 --- a/source/blender/windowmanager/xr/intern/wm_xr_operators.cc +++ b/source/blender/windowmanager/xr/intern/wm_xr_operators.cc @@ -37,6 +37,10 @@ #include "GPU_immediate.hh" #include "GPU_state.hh" +#include "GPU_batch_presets.hh" +#include "GPU_matrix.hh" +#include "GPU_xr_defines.hh" + #include "MEM_guardedalloc.h" #include "RNA_access.hh" @@ -529,6 +533,11 @@ static void wm_xr_navigation_grab_bimanual_state_update(const wmXrActionData *ac } } +static void wm_xr_navigation_grab_cancel(bContext * /*C*/, wmOperator *op) +{ + wm_xr_grab_uninit(op); +} + static wmOperatorStatus wm_xr_navigation_grab_modal(bContext *C, wmOperator *op, const wmEvent *event) @@ -542,6 +551,8 @@ static wmOperatorStatus wm_xr_navigation_grab_modal(bContext *C, wmWindowManager *wm = CTX_wm_manager(C); wmXrData *xr = &wm->xr; + WM_xr_session_state_vignette_activate(xr); + const bool do_bimanual = wm_xr_navigation_grab_can_do_bimanual(actiondata, data); data->loc_lock = RNA_boolean_get(op->ptr, "lock_location"); @@ -588,6 +599,7 @@ static void WM_OT_xr_navigation_grab(wmOperatorType *ot) /* Callbacks. */ ot->invoke = wm_xr_navigation_grab_invoke; ot->exec = wm_xr_navigation_grab_exec; + ot->cancel = wm_xr_navigation_grab_cancel; ot->modal = wm_xr_navigation_grab_modal; ot->poll = wm_xr_operator_sessionactive; @@ -613,21 +625,60 @@ static void WM_OT_xr_navigation_grab(wmOperatorType *ot) * \{ */ static const float g_xr_default_raycast_axis[3] = {0.0f, 0.0f, -1.0f}; -static const float g_xr_default_raycast_color[4] = {0.35f, 0.35f, 1.0f, 1.0f}; +static const float g_xr_default_raycast_hit_color[4] = {0.35f, 0.35f, 1.0f, 1.0f}; +static const float g_xr_default_raycast_miss_color[4] = {1.0f, 0.35f, 0.35f, 1.0f}; +static const float g_xr_default_raycast_fallback_color[4] = {0.35f, 0.35f, 1.0f, 1.0f}; + +enum XrRaycastResult : uint8_t { + XR_RAYCAST_MISS, + XR_RAYCAST_HIT, + XR_RAYCAST_FALLBACK, +}; struct XrRaycastData { + /** Raycast info */ bool from_viewer; - float origin[3]; + + /** Raycast results */ + XrRaycastResult result; + int num_points; + float points[XR_MAX_RAYCASTS + 1][4]; float direction[3]; - float end[3]; + + /** Raycast visualization parameters */ float color[4]; + float raycast_width; + float destination_size; + int sample_count; + + blender::gpu::Batch *raycast_model; + void *draw_handle; }; +static void wm_xr_raycast_destination_draw(const XrRaycastData *data) +{ + GPU_depth_test(GPU_DEPTH_LESS_EQUAL); + + blender::gpu::Batch *sphere = GPU_batch_preset_sphere(2); + GPU_batch_program_set_builtin(sphere, GPU_SHADER_3D_UNIFORM_COLOR); + GPU_batch_uniform_4fv(sphere, "color", data->color); + + GPU_matrix_push(); + GPU_matrix_translate_3fv(data->points[data->num_points - 1]); + GPU_matrix_scale_1f(data->destination_size); + GPU_batch_draw(sphere); + GPU_matrix_pop(); +} + static void wm_xr_raycast_draw(const bContext * /*C*/, ARegion * /*region*/, void *customdata) { const XrRaycastData *data = static_cast(customdata); + if (data->result != XR_RAYCAST_MISS) { + wm_xr_raycast_destination_draw(data); + } + GPUVertFormat *format = immVertexFormat(); uint pos = GPU_vertformat_attr_add(format, "pos", blender::gpu::VertAttrType::SFLOAT_32_32_32); @@ -638,32 +689,37 @@ static void wm_xr_raycast_draw(const bContext * /*C*/, ARegion * /*region*/, voi GPU_depth_test(GPU_DEPTH_NONE); GPU_point_size(7.0f); - immBegin(GPU_PRIM_POINTS, 1); - immVertex3fv(pos, data->end); + immBegin(GPU_PRIM_POINTS, data->num_points - 1); + + for (int i = 1; i < data->num_points; ++i) { + immVertex3fv(pos, data->points[i]); + } + immEnd(); + immUnbindProgram(); } else { - uint col = GPU_vertformat_attr_add( - format, "color", blender::gpu::VertAttrType::SFLOAT_32_32_32_32); - immBindBuiltinProgram(GPU_SHADER_3D_POLYLINE_FLAT_COLOR); + BLI_assert(data->raycast_model != nullptr); - float viewport[4]; - GPU_viewport_size_get_f(viewport); - immUniform2fv("viewportSize", &viewport[2]); + float forward[3]; + float right[3]; - immUniform1f("lineWidth", 3.0f * U.pixelsize); + sub_v3_v3v3(forward, data->points[data->num_points - 1], data->points[0]); + copy_v3_fl3(right, forward[1], -forward[0], 0.0f); + normalize_v3(right); GPU_depth_test(GPU_DEPTH_LESS_EQUAL); - immBegin(GPU_PRIM_LINES, 2); - immAttrSkip(col); - immVertex3fv(pos, data->origin); - immAttr4fv(col, data->color); - immVertex3fv(pos, data->end); - immEnd(); + GPU_batch_program_set_builtin(data->raycast_model, GPU_SHADER_XR_RAYCAST); + GPU_batch_uniform_4fv_array( + data->raycast_model, "control_points", XR_MAX_RAYCASTS + 1, data->points); + GPU_batch_uniform_4fv(data->raycast_model, "color", data->color); + GPU_batch_uniform_3fv(data->raycast_model, "right_vector", right); + GPU_batch_uniform_1f(data->raycast_model, "width", data->raycast_width); + GPU_batch_uniform_1i(data->raycast_model, "control_point_count", data->num_points); + GPU_batch_uniform_1i(data->raycast_model, "sample_count", data->sample_count); + GPU_batch_draw(data->raycast_model); } - - immUnbindProgram(); } static void wm_xr_raycast_init(wmOperator *op) @@ -712,27 +768,34 @@ static void wm_xr_raycast_update(wmOperator *op, const wmXrActionData *actiondata) { XrRaycastData *data = static_cast(op->customdata); - float ray_length, axis[3]; + float axis[3], nav_scale; + + WM_xr_session_state_nav_scale_get(xr, &nav_scale); data->from_viewer = RNA_boolean_get(op->ptr, "from_viewer"); + data->raycast_width = RNA_float_get(op->ptr, "raycast_scale") * nav_scale; + data->sample_count = RNA_int_get(op->ptr, "sample_count"); RNA_float_get_array(op->ptr, "axis", axis); - RNA_float_get_array(op->ptr, "color", data->color); if (data->from_viewer) { float viewer_rot[4]; - WM_xr_session_state_viewer_pose_location_get(xr, data->origin); + WM_xr_session_state_viewer_pose_location_get(xr, data->points[0]); WM_xr_session_state_viewer_pose_rotation_get(xr, viewer_rot); mul_qt_v3(viewer_rot, axis); - ray_length = (xr->session_settings.clip_start + xr->session_settings.clip_end) / 2.0f; } else { - copy_v3_v3(data->origin, actiondata->controller_loc); + if (!xr->runtime->session_state.raycast_model) { + xr->runtime->session_state.raycast_model = GPU_batch_create_procedural( + GPU_PRIM_TRI_STRIP, 2 * data->sample_count); + } + + data->raycast_model = xr->runtime->session_state.raycast_model; + + copy_v3_v3(data->points[0], actiondata->controller_loc); mul_qt_v3(actiondata->controller_rot, axis); - ray_length = xr->session_settings.clip_end; } copy_v3_v3(data->direction, axis); - madd_v3_v3v3fl(data->end, data->origin, data->direction, ray_length); } static void wm_xr_raycast(Scene *scene, @@ -779,8 +842,6 @@ static void wm_xr_raycast(Scene *scene, * controller. * \{ */ -#define XR_DEFAULT_FLY_SPEED_MOVE 0.054f - enum eXrFlyMode { XR_FLY_FORWARD = 0, XR_FLY_BACK = 1, @@ -800,6 +861,9 @@ enum eXrFlyMode { struct XrFlyData { float viewer_rot[4]; double time_prev; + + /* Only used for snap turn, where the action should be executed only once. */ + bool is_finished; }; static void wm_xr_fly_init(wmOperator *op, const wmXrData *xr) @@ -946,6 +1010,11 @@ static wmOperatorStatus wm_xr_navigation_fly_exec(bContext * /*C*/, wmOperator * return OPERATOR_CANCELLED; } +static void wm_xr_navigation_fly_cancel(bContext * /*C*/, wmOperator *op) +{ + wm_xr_fly_uninit(op); +} + static wmOperatorStatus wm_xr_navigation_fly_modal(bContext *C, wmOperator *op, const wmEvent *event) @@ -964,22 +1033,41 @@ static wmOperatorStatus wm_xr_navigation_fly_modal(bContext *C, wmWindowManager *wm = CTX_wm_manager(C); wmXrData *xr = &wm->xr; eXrFlyMode mode; - bool turn, locz_lock, dir_lock, speed_frame_based; + bool turn, snap_turn, invert_rotation, swap_hands, locz_lock, dir_lock, speed_frame_based; bool speed_interp_cubic = false; - float speed, speed_max, speed_p0[2], speed_p1[2]; + float speed, speed_max, speed_p0[2], speed_p1[2], button_state; GHOST_XrPose nav_pose; float nav_mat[4][4], delta[4][4], out[4][4]; - const double time_now = BLI_time_now_seconds(); + const double time_now = BLI_time_now_seconds(), delta_time = time_now - data->time_prev; + data->time_prev = time_now; - mode = (eXrFlyMode)RNA_enum_get(op->ptr, "mode"); + swap_hands = xr->runtime->session_state.swap_hands; + mode = (eXrFlyMode)RNA_enum_get(op->ptr, swap_hands ? "alt_mode" : "mode"); turn = ELEM(mode, XR_FLY_TURNLEFT, XR_FLY_TURNRIGHT); + snap_turn = U.xr_navigation.flag & USER_XR_NAV_SNAP_TURN; + invert_rotation = U.xr_navigation.flag & USER_XR_NAV_INVERT_ROTATION; - locz_lock = RNA_boolean_get(op->ptr, "lock_location_z"); - dir_lock = RNA_boolean_get(op->ptr, "lock_direction"); - speed_frame_based = RNA_boolean_get(op->ptr, "speed_frame_based"); - speed = RNA_float_get(op->ptr, "speed_min"); - speed_max = RNA_float_get(op->ptr, "speed_max"); + locz_lock = RNA_boolean_get(op->ptr, swap_hands ? "alt_lock_location_z" : "lock_location_z"); + dir_lock = RNA_boolean_get(op->ptr, swap_hands ? "alt_lock_direction" : "lock_direction"); + + if (turn) { + speed_frame_based = false; + + if (snap_turn) { + speed_max = U.xr_navigation.turn_amount; + speed = speed_max; + } + else { + speed_max = U.xr_navigation.turn_speed; + speed = speed_max * RNA_boolean_get(op->ptr, "turn_speed_factor"); + } + } + else { + speed_frame_based = RNA_boolean_get(op->ptr, "speed_frame_based"); + speed_max = xr->session_settings.fly_speed; + speed = speed_max * RNA_float_get(op->ptr, "fly_speed_factor"); + } PropertyRNA *prop = RNA_struct_find_property(op->ptr, "speed_interpolation0"); if (prop && RNA_property_is_set(op->ptr, prop)) { @@ -1007,14 +1095,15 @@ static wmOperatorStatus wm_xr_navigation_fly_modal(bContext *C, /* Interpolate between min/max speeds based on button state. */ switch (actiondata->type) { case XR_BOOLEAN_INPUT: + button_state = 1.0f; speed = speed_max; break; case XR_FLOAT_INPUT: case XR_VECTOR2F_INPUT: { - float state = (actiondata->type == XR_FLOAT_INPUT) ? fabsf(actiondata->state[0]) : - len_v2(actiondata->state); + button_state = (actiondata->type == XR_FLOAT_INPUT) ? fabsf(actiondata->state[0]) : + len_v2(actiondata->state); float speed_t = (actiondata->float_threshold < 1.0f) ? - (state - actiondata->float_threshold) / + (button_state - actiondata->float_threshold) / (1.0f - actiondata->float_threshold) : 1.0f; if (speed_interp_cubic) { @@ -1041,21 +1130,29 @@ static wmOperatorStatus wm_xr_navigation_fly_modal(bContext *C, break; } - if (!speed_frame_based) { - /* Adjust speed based on last update time. */ - speed *= time_now - data->time_prev; - } - data->time_prev = time_now; - WM_xr_session_state_nav_location_get(xr, nav_pose.position); WM_xr_session_state_nav_rotation_get(xr, nav_pose.orientation_quat); wm_xr_pose_to_mat(&nav_pose, nav_mat); if (turn) { - if (dir_lock) { + if (dir_lock || (snap_turn && data->is_finished) || + (snap_turn && button_state < RNA_float_get(op->ptr, "snap_turn_threshold"))) + { unit_m4(delta); } else { + if (!snap_turn) { + WM_xr_session_state_vignette_activate(xr); + speed *= delta_time; + } + else { + data->is_finished = true; + } + + if (invert_rotation) { + speed *= -1.0f; + } + GHOST_XrPose viewer_pose; float viewer_mat[4][4], nav_inv[4][4]; @@ -1070,10 +1167,16 @@ static wmOperatorStatus wm_xr_navigation_fly_modal(bContext *C, else { float nav_scale, ref_quat[4]; + WM_xr_session_state_vignette_activate(xr); + /* Adjust speed for base and navigation scale. */ WM_xr_session_state_nav_scale_get(xr, &nav_scale); speed *= xr->session_settings.base_scale * nav_scale; + if (!speed_frame_based) { + speed *= delta_time; + } + switch (mode) { /* Move relative to navigation space. */ case XR_FLY_FORWARD: @@ -1140,6 +1243,7 @@ static void WM_OT_xr_navigation_fly(wmOperatorType *ot) /* Callbacks. */ ot->invoke = wm_xr_navigation_fly_invoke; ot->exec = wm_xr_navigation_fly_exec; + ot->cancel = wm_xr_navigation_fly_cancel; ot->modal = wm_xr_navigation_fly_modal; ot->poll = wm_xr_operator_sessionactive; @@ -1179,6 +1283,15 @@ static void WM_OT_xr_navigation_fly(wmOperatorType *ot) prop = RNA_def_enum(ot->srna, "mode", fly_modes, XR_FLY_VIEWER_FORWARD, "Mode", "Fly mode"); RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_NAVIGATION); + RNA_def_float(ot->srna, + "snap_turn_threshold", + 0.95f, + 0.0f, + 1.0f, + "Snap Turn Threshold", + "Input state threshold when using snap turn", + 0.0f, + 1.0f); RNA_def_boolean( ot->srna, "lock_location_z", false, "Lock Elevation", "Prevent changes to viewer elevation"); RNA_def_boolean(ot->srna, @@ -1188,27 +1301,27 @@ static void WM_OT_xr_navigation_fly(wmOperatorType *ot) "Limit movement to viewer's initial direction"); RNA_def_boolean(ot->srna, "speed_frame_based", - true, + false, "Frame Based Speed", "Apply fixed movement deltas every update"); RNA_def_float(ot->srna, - "speed_min", - XR_DEFAULT_FLY_SPEED_MOVE / 3.0f, + "turn_speed_factor", + 1.0 / 3.0f, 0.0f, - 1000.0f, - "Minimum Speed", - "Minimum move (turn) speed in meters (radians) per second or frame", + 1.0f, + "Turn Speed Factor", + "Ratio between the min and max turn speed", 0.0f, - 1000.0f); + 1.0f); RNA_def_float(ot->srna, - "speed_max", - XR_DEFAULT_FLY_SPEED_MOVE, + "fly_speed_factor", + 1.0 / 3.0f, 0.0f, - 1000.0f, - "Maximum Speed", - "Maximum move (turn) speed in meters (radians) per second or frame", + 1.0f, + "Fly Speed Factor", + "Ratio between the min and max fly speed", 0.0f, - 1000.0f); + 1.0f); RNA_def_float_vector(ot->srna, "speed_interpolation0", 2, @@ -1229,6 +1342,23 @@ static void WM_OT_xr_navigation_fly(wmOperatorType *ot) "Second cubic spline control point between min/max speeds", 0.0f, 1.0f); + + RNA_def_enum(ot->srna, + "alt_mode", + fly_modes, + XR_FLY_VIEWER_FORWARD, + "Mode (Alt)", + "Fly mode when hands are swapped"); + RNA_def_boolean(ot->srna, + "alt_lock_location_z", + false, + "Lock Elevation (Alt)", + "When hands are swapped, prevent changes to viewer elevation"); + RNA_def_boolean(ot->srna, + "alt_lock_direction", + false, + "Lock Direction (Alt)", + "When hands are swapped, limit movement to viewer's initial direction"); } /** \} */ @@ -1239,68 +1369,221 @@ static void WM_OT_xr_navigation_fly(wmOperatorType *ot) * Casts a ray from an XR controller's pose and teleports to any hit geometry. * \{ */ -static void wm_xr_navigation_teleport(bContext *C, - wmXrData *xr, - const float origin[3], - const float direction[3], - float *ray_dist, - bool selectable_only, - const bool teleport_axes[3], - float teleport_t, - float teleport_ofs) +static float wm_xr_navigation_teleport_pose_calc(wmXrData *xr, + float nav_destination[3], + const float destination[4], + const float normal[3], + const bool teleport_axes[3], + float teleport_t, + float teleport_ofs, + float vertical_ofs) +{ + float nav_location[3], nav_rotation[4], viewer_location[3]; + WM_xr_session_state_nav_location_get(xr, nav_location); + WM_xr_session_state_nav_rotation_get(xr, nav_rotation); + WM_xr_session_state_viewer_pose_location_get(xr, viewer_location); + + float nav_axes[3][3], projected[3], v0[3], v1[3], destination_with_ofs[3]; + + copy_v3_fl(nav_destination, 0.0f); + copy_v3_v3(destination_with_ofs, destination); + destination_with_ofs[2] += vertical_ofs; + + wm_xr_basenav_rotation_calc(xr, nav_rotation, nav_rotation); + quat_to_mat3(nav_axes, nav_rotation); + + /* Project locations onto navigation axes. */ + for (int a = 0; a < 3; ++a) { + project_v3_v3v3_normalized(projected, nav_location, nav_axes[a]); + if (teleport_axes[a]) { + /* Interpolate between projected locations. */ + project_v3_v3v3_normalized(v0, destination_with_ofs, nav_axes[a]); + project_v3_v3v3_normalized(v1, viewer_location, nav_axes[a]); + sub_v3_v3(v0, v1); + madd_v3_v3fl(projected, v0, teleport_t); + /* Subtract offset. */ + project_v3_v3v3_normalized(v0, normal, nav_axes[a]); + madd_v3_v3fl(projected, v0, teleport_ofs); + } + /* Add to final location. */ + add_v3_v3(nav_destination, projected); + } + + return len_v3v3(viewer_location, destination); +} + +static bool wm_xr_navigation_teleport_ground_plane(float points[XR_MAX_RAYCASTS + 1][4], + int *num_points, + float *ray_dist) +{ + constexpr uint z = 2; + for (int i = 1; i < *num_points; ++i) { + float *startpoint = points[i - 1], *endpoint = points[i]; + + if ((startpoint[z] < 0) == (endpoint[z] < 0)) { + continue; + } + + if (startpoint[z] == endpoint[z]) { + break; + } + + float segment_ray_dist = len_v3v3(startpoint, endpoint); + float alpha = startpoint[z] / (startpoint[z] - endpoint[z]); + interp_v3_v3v3(endpoint, startpoint, endpoint, alpha); + + *ray_dist = segment_ray_dist * (i - 1) + len_v3v3(startpoint, endpoint); + *num_points = i + 1; + return true; + } + + return false; +} + +static XrRaycastResult wm_xr_navigation_teleport(bContext *C, + wmXrData *xr, + float nav_destination[3], + float points[XR_MAX_RAYCASTS + 1][4], + const float direction[3], + int *num_points, + float *ray_dist, + float *destination_dist, + bool selectable_only, + const bool teleport_axes[3], + float teleport_t, + float teleport_ofs, + float gravity, + float head_height) { Scene *scene = CTX_data_scene(C); Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); - float location[3]; - float normal[3]; int index; const Object *ob = nullptr; float obmat[4][4]; - wm_xr_raycast(scene, - depsgraph, - origin, - direction, - ray_dist, - selectable_only, - location, - normal, - &index, - &ob, - obmat); + float normal[3], segment_direction[3]; + float vertical_ofs = 0; + XrRaycastResult result = XR_RAYCAST_MISS; - /* Teleport. */ - if (ob) { - float nav_location[3], nav_rotation[4], viewer_location[3]; - float nav_axes[3][3], projected[3], v0[3], v1[3]; - float out[3] = {0.0f, 0.0f, 0.0f}; + copy_v3_v3(segment_direction, direction); + copy_v3_fl3(normal, 0, 1, 0); - WM_xr_session_state_nav_location_get(xr, nav_location); - WM_xr_session_state_nav_rotation_get(xr, nav_rotation); - WM_xr_session_state_viewer_pose_location_get(xr, viewer_location); + /* When ray_dist == 0 or -1, the raycast is a line of infinite length. */ + if (*ray_dist <= 0.0f) { + *num_points = 2; + } - wm_xr_basenav_rotation_calc(xr, nav_rotation, nav_rotation); - quat_to_mat3(nav_axes, nav_rotation); + const float segment_length = *ray_dist / (*num_points - 1); + float segment_ray_dist = 0.0f; + *ray_dist = 0.0f; - /* Project locations onto navigation axes. */ - for (int a = 0; a < 3; ++a) { - project_v3_v3v3_normalized(projected, nav_location, nav_axes[a]); - if (teleport_axes[a]) { - /* Interpolate between projected locations. */ - project_v3_v3v3_normalized(v0, location, nav_axes[a]); - project_v3_v3v3_normalized(v1, viewer_location, nav_axes[a]); - sub_v3_v3(v0, v1); - madd_v3_v3fl(projected, v0, teleport_t); - /* Subtract offset. */ - project_v3_v3v3_normalized(v0, normal, nav_axes[a]); - madd_v3_v3fl(projected, v0, teleport_ofs); + for (int i = 1; i < *num_points; ++i) { + segment_ray_dist = segment_length; + wm_xr_raycast(scene, + depsgraph, + points[i - 1], + segment_direction, + &segment_ray_dist, + selectable_only, + points[i], + normal, + &index, + &ob, + obmat); + + *ray_dist += segment_ray_dist; + + if (ob) { + *num_points = i + 1; + + /** Ensure normal faces the correct direction */ + if (dot_v3v3(segment_direction, normal) > 0) { + mul_v3_fl(normal, -1.0f); } - /* Add to final location. */ - add_v3_v3(out, projected); + + result = XR_RAYCAST_HIT; + break; } - WM_xr_session_state_nav_location_set(xr, out); + madd_v3_v3v3fl(points[i], points[i - 1], segment_direction, segment_length); + + /* Apply gravity */ + segment_direction[2] -= gravity; + normalize_v3(segment_direction); } + + /** Fall back to raycast intersecting with the ground plane. */ + if (result == XR_RAYCAST_MISS) { + vertical_ofs = head_height; + + if (wm_xr_navigation_teleport_ground_plane(points, num_points, ray_dist)) { + result = XR_RAYCAST_FALLBACK; + } + } + + if (result != XR_RAYCAST_MISS) { + float origin[3], dummy_dest[3], dummy_normal[3]; + + /* Raycast downward to see if we're on the floor */ + copy_v3_fl3(segment_direction, 0, 0, -1); + + copy_v3_v3(origin, points[*num_points - 1]); + madd_v3_v3fl(origin, normal, teleport_ofs); + madd_v3_v3fl(origin, segment_direction, -vertical_ofs); + + segment_ray_dist = head_height; + ob = nullptr; + wm_xr_raycast(scene, + depsgraph, + origin, + segment_direction, + &segment_ray_dist, + selectable_only, + dummy_dest, + dummy_normal, + &index, + &ob, + obmat); + + /* Raycast upward to make sure we don't clip through the ceiling */ + if (ob) { + vertical_ofs = head_height - segment_ray_dist; + copy_v3_fl3(segment_direction, 0, 0, 1); + + copy_v3_v3(origin, points[*num_points - 1]); + madd_v3_v3fl(origin, normal, teleport_ofs); + + segment_ray_dist = vertical_ofs; + ob = nullptr; + wm_xr_raycast(scene, + depsgraph, + origin, + segment_direction, + &segment_ray_dist, + selectable_only, + dummy_dest, + dummy_normal, + &index, + &ob, + obmat); + + if (ob) { + vertical_ofs = max_ff(0.0f, segment_ray_dist - teleport_ofs); + } + } + + /* Calculate teleportation destination in navigation space */ + *destination_dist = wm_xr_navigation_teleport_pose_calc(xr, + nav_destination, + points[*num_points - 1], + normal, + teleport_axes, + teleport_t, + teleport_ofs, + vertical_ofs); + } + + return result; } static wmOperatorStatus wm_xr_navigation_teleport_invoke(bContext *C, @@ -1328,6 +1611,11 @@ static wmOperatorStatus wm_xr_navigation_teleport_exec(bContext * /*C*/, wmOpera return OPERATOR_CANCELLED; } +static void wm_xr_navigation_teleport_cancel(bContext * /*C*/, wmOperator *op) +{ + wm_xr_raycast_uninit(op); +} + static wmOperatorStatus wm_xr_navigation_teleport_modal(bContext *C, wmOperator *op, const wmEvent *event) @@ -1340,32 +1628,66 @@ static wmOperatorStatus wm_xr_navigation_teleport_modal(bContext *C, wmWindowManager *wm = CTX_wm_manager(C); wmXrData *xr = &wm->xr; + xr->runtime->session_state.is_raycast_shown = true; wm_xr_raycast_update(op, xr, actiondata); + XrRaycastData *data = static_cast(op->customdata); + float nav_scale, ray_dist, destination_dist, nav_destination[3]; + bool teleport_axes[3]; + + WM_xr_session_state_nav_scale_get(xr, &nav_scale); + + RNA_boolean_get_array(op->ptr, "teleport_axes", teleport_axes); + const float teleport_t = RNA_float_get(op->ptr, "interpolation"); + const float teleport_ofs = RNA_float_get(op->ptr, "offset") * nav_scale; + const float gravity = RNA_float_get(op->ptr, "gravity"); + const float head_height = xr->runtime->session_state.prev_local_pose.position[1] * nav_scale; + const bool selectable_only = RNA_boolean_get(op->ptr, "selectable_only"); + ray_dist = RNA_float_get(op->ptr, "distance") * nav_scale; + + data->num_points = XR_MAX_RAYCASTS + 1; + data->result = wm_xr_navigation_teleport(C, + xr, + nav_destination, + data->points, + data->direction, + &data->num_points, + &ray_dist, + &destination_dist, + selectable_only, + teleport_axes, + teleport_t, + teleport_ofs, + gravity, + head_height); + + data->destination_size = RNA_float_get(op->ptr, "destination_scale") * + sqrt(destination_dist / nav_scale) * nav_scale; + + switch (data->result) { + case XR_RAYCAST_MISS: + RNA_float_get_array(op->ptr, "miss_color", data->color); + break; + case XR_RAYCAST_HIT: + RNA_float_get_array(op->ptr, "hit_color", data->color); + break; + case XR_RAYCAST_FALLBACK: + RNA_float_get_array(op->ptr, "fallback_color", data->color); + break; + default: + BLI_assert_unreachable(); + break; + } + switch (event->val) { case KM_PRESS: return OPERATOR_RUNNING_MODAL; case KM_RELEASE: { - XrRaycastData *data = static_cast(op->customdata); - bool selectable_only, teleport_axes[3]; - float teleport_t, teleport_ofs, ray_dist; - - RNA_boolean_get_array(op->ptr, "teleport_axes", teleport_axes); - teleport_t = RNA_float_get(op->ptr, "interpolation"); - teleport_ofs = RNA_float_get(op->ptr, "offset"); - selectable_only = RNA_boolean_get(op->ptr, "selectable_only"); - ray_dist = RNA_float_get(op->ptr, "distance"); - - wm_xr_navigation_teleport(C, - xr, - data->origin, - data->direction, - &ray_dist, - selectable_only, - teleport_axes, - teleport_t, - teleport_ofs); + if (data->result != XR_RAYCAST_MISS) { + WM_xr_session_state_nav_location_set(xr, nav_destination); + } + xr->runtime->session_state.is_raycast_shown = false; wm_xr_raycast_uninit(op); return OPERATOR_FINISHED; @@ -1388,6 +1710,7 @@ static void WM_OT_xr_navigation_teleport(wmOperatorType *ot) /* Callbacks. */ ot->invoke = wm_xr_navigation_teleport_invoke; ot->exec = wm_xr_navigation_teleport_exec; + ot->cancel = wm_xr_navigation_teleport_cancel; ot->modal = wm_xr_navigation_teleport_modal; ot->poll = wm_xr_operator_sessionactive; @@ -1411,7 +1734,7 @@ static void WM_OT_xr_navigation_teleport(wmOperatorType *ot) 1.0f); RNA_def_float(ot->srna, "offset", - 0.0f, + 0.25f, 0.0f, FLT_MAX, "Offset", @@ -1425,13 +1748,49 @@ static void WM_OT_xr_navigation_teleport(wmOperatorType *ot) "Only allow selectable objects to influence raycast result"); RNA_def_float(ot->srna, "distance", - BVH_RAYCAST_DIST_MAX, + 80.0, 0.0, BVH_RAYCAST_DIST_MAX, "", "Maximum raycast distance", 0.0, BVH_RAYCAST_DIST_MAX); + RNA_def_float(ot->srna, + "gravity", + 0.1, + 0.0, + FLT_MAX, + "Gravity", + "Downward curvature applied to raycast", + 0.0, + FLT_MAX); + RNA_def_float(ot->srna, + "raycast_scale", + 0.02f, + 0.0f, + FLT_MAX, + "Raycast Scale", + "Width of the raycast visualization", + 0.0f, + FLT_MAX); + RNA_def_float(ot->srna, + "destination_scale", + 0.05f, + 0.0f, + FLT_MAX, + "Destination Scale", + "Width of the destination visualization", + 0.0f, + FLT_MAX); + RNA_def_int(ot->srna, + "sample_count", + 48, + 2, + INT_MAX, + "Sample Count", + "Number of interpolation samples for the raycast visualization", + 2, + INT_MAX); RNA_def_boolean( ot->srna, "from_viewer", false, "From Viewer", "Use viewer pose as raycast origin"); RNA_def_float_vector(ot->srna, @@ -1445,13 +1804,33 @@ static void WM_OT_xr_navigation_teleport(wmOperatorType *ot) -1.0f, 1.0f); RNA_def_float_color(ot->srna, - "color", + "hit_color", 4, - g_xr_default_raycast_color, + g_xr_default_raycast_hit_color, 0.0f, 1.0f, - "Color", - "Raycast color", + "Hit Color", + "Color of raycast when it succeeds", + 0.0f, + 1.0f); + RNA_def_float_color(ot->srna, + "miss_color", + 4, + g_xr_default_raycast_miss_color, + 0.0f, + 1.0f, + "Miss Color", + "Color of raycast when it misses", + 0.0f, + 1.0f); + RNA_def_float_color(ot->srna, + "fallback_color", + 4, + g_xr_default_raycast_fallback_color, + 0.0f, + 1.0f, + "Fallback Color", + "Color of raycast when a fallback case succeeds", 0.0f, 1.0f); } @@ -1547,6 +1926,74 @@ static void WM_OT_xr_navigation_reset(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name XR Navigation Swap Hands + * + * Resets XR navigation deltas relative to session base pose. + * \{ */ + +static wmOperatorStatus wm_xr_navigation_swap_hands_invoke(bContext *C, + wmOperator *op, + const wmEvent *event) +{ + if (!wm_xr_operator_test_event(op, event)) { + return OPERATOR_PASS_THROUGH; + } + + WM_event_add_modal_handler(C, op); + + wmWindowManager *wm = CTX_wm_manager(C); + wmXrData *xr = &wm->xr; + + xr->runtime->session_state.swap_hands = true; + + return OPERATOR_RUNNING_MODAL; +} + +static wmOperatorStatus wm_xr_navigation_swap_hands_exec(bContext * /*C*/, wmOperator * /*op*/) +{ + return OPERATOR_CANCELLED; +} + +static wmOperatorStatus wm_xr_navigation_swap_hands_modal(bContext *C, + wmOperator *op, + const wmEvent *event) +{ + if (!wm_xr_operator_test_event(op, event)) { + return OPERATOR_PASS_THROUGH; + } + + wmWindowManager *wm = CTX_wm_manager(C); + wmXrData *xr = &wm->xr; + + switch (event->val) { + case KM_PRESS: + return OPERATOR_RUNNING_MODAL; + case KM_RELEASE: + xr->runtime->session_state.swap_hands = false; + return OPERATOR_FINISHED; + default: + BLI_assert_unreachable(); + return OPERATOR_CANCELLED; + } +} + +static void WM_OT_xr_navigation_swap_hands(wmOperatorType *ot) +{ + /* Identifiers. */ + ot->name = "XR Navigation Swap Hands"; + ot->idname = "WM_OT_xr_navigation_swap_hands"; + ot->description = "Swap VR navigation controls between left / right controllers"; + + /* Callbacks. */ + ot->invoke = wm_xr_navigation_swap_hands_invoke; + ot->exec = wm_xr_navigation_swap_hands_exec; + ot->modal = wm_xr_navigation_swap_hands_modal; + ot->poll = wm_xr_operator_sessionactive; +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Operator Registration * \{ */ @@ -1558,6 +2005,7 @@ void wm_xr_operatortypes_register() WM_operatortype_append(WM_OT_xr_navigation_fly); WM_operatortype_append(WM_OT_xr_navigation_teleport); WM_operatortype_append(WM_OT_xr_navigation_reset); + WM_operatortype_append(WM_OT_xr_navigation_swap_hands); } /** \} */ diff --git a/source/blender/windowmanager/xr/intern/wm_xr_session.cc b/source/blender/windowmanager/xr/intern/wm_xr_session.cc index 847c26e2fd1..d5173d24f31 100644 --- a/source/blender/windowmanager/xr/intern/wm_xr_session.cc +++ b/source/blender/windowmanager/xr/intern/wm_xr_session.cc @@ -71,6 +71,10 @@ static void wm_xr_session_create_cb() settings->base_scale = 1.0f; } state->prev_base_scale = settings->base_scale; + + /* Initialize vignette. */ + state->vignette_data = MEM_callocN(__func__); + WM_xr_session_state_vignette_reset(state); } static void wm_xr_session_controller_data_free(wmXrSessionState *state) @@ -84,9 +88,27 @@ static void wm_xr_session_controller_data_free(wmXrSessionState *state) } } +static void wm_xr_session_vignette_data_free(wmXrSessionState *state) +{ + if (state->vignette_data) { + MEM_freeN(state->vignette_data); + state->vignette_data = nullptr; + } +} + +static void wm_xr_session_raycast_model_free(wmXrSessionState *state) +{ + if (state->raycast_model) { + GPU_batch_discard(state->raycast_model); + state->raycast_model = nullptr; + } +} + void wm_xr_session_data_free(wmXrSessionState *state) { wm_xr_session_controller_data_free(state); + wm_xr_session_vignette_data_free(state); + wm_xr_session_raycast_model_free(state); } static void wm_xr_session_exit_cb(void *customdata) @@ -384,6 +406,8 @@ void wm_xr_session_state_update(const XrSessionSettings *settings, state->is_view_data_set = true; /* Assume this was already done through wm_xr_session_draw_data_update(). */ state->force_reset_to_base_pose = false; + + WM_xr_session_state_vignette_update(state); } wmXrSessionState *WM_xr_session_state_handle_get(const wmXrData *xr) @@ -572,6 +596,52 @@ void WM_xr_session_state_navigation_reset(wmXrSessionState *state) unit_qt(state->nav_pose.orientation_quat); state->nav_scale = 1.0f; state->is_navigation_dirty = true; + state->swap_hands = false; +} + +void WM_xr_session_state_vignette_reset(wmXrSessionState *state) +{ + wmXrVignetteData *data = state->vignette_data; + + /* Reset vignette state */ + data->aperture = 1.0f; + data->aperture_velocity = 0.0f; + + /* Set default vignette parameters */ + data->initial_aperture = 0.25f; + data->initial_aperture_velocity = -0.03f; + + data->aperture_min = 0.08f; + data->aperture_max = 0.3f; + + data->aperture_velocity_max = 0.002f; + data->aperture_velocity_delta = 0.01f; +} + +void WM_xr_session_state_vignette_activate(wmXrData *xr) +{ + if (WM_xr_session_exists(xr)) { + wmXrVignetteData *data = xr->runtime->session_state.vignette_data; + data->aperture_velocity = data->initial_aperture_velocity; + data->aperture = min_ff(data->aperture, data->initial_aperture); + } +} + +void WM_xr_session_state_vignette_update(wmXrSessionState *state) +{ + wmXrVignetteData *data = state->vignette_data; + + const float vignette_intensity = U.xr_navigation.vignette_intensity; + const float aperture_min = interpf( + data->aperture_min, data->aperture_max, vignette_intensity * 0.01f); + data->aperture_velocity = min_ff(data->aperture_velocity_max, + data->aperture_velocity + data->aperture_velocity_delta); + + if (data->aperture == aperture_min) { + data->aperture_velocity = data->aperture_velocity_max; + } + + data->aperture = clamp_f(data->aperture + data->aperture_velocity, aperture_min, 1.0f); } /* -------------------------------------------------------------------- */