VR: Location Scouting

This feature commit implements VR Location Scouting within Blender,
allowing the user to block camera vantage points while immersed in VR.

For feature design, see:

- VR: Location Scouting (!141089): https://projects.blender.org/blender/blender/issues/141089
- VR Location Scouting - high fidelity UI (#143061): https://projects.blender.org/blender/blender/issues/143061
- Design Session on Devtalk: https://devtalk.blender.org/t/2025-06-19-design-session-vr-location-scouting/41135

For up to date screenshots, demos and discussion regarding the feature,
see the following devtalk feedback thread:
https://devtalk.blender.org/t/vr-location-scouting-feedback-and-discussion/44744

Pull Request: https://projects.blender.org/blender/blender/pulls/143945
This commit is contained in:
Jonas Holzman 2026-06-02 18:16:49 +02:00
parent a1228b3187
commit 0ee0341936
25 changed files with 4648 additions and 63 deletions

View file

@ -1429,6 +1429,708 @@ actionconfig_data = [
],
},
),
(
"viewfinder_capture",
{
"type": "FLOAT",
"user_paths": ["/user/hand/left", "/user/hand/right"],
"op": "view3d.vr_location_scouting_viewfinder_capture",
"op_mode": "PRESS",
"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": []},
{
"bindings": [
(
"huawei",
{
"profile": "/interaction_profiles/huawei/controller",
"component_paths": [
"/input/trigger/value",
"/input/trigger/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"index",
{
"profile": "/interaction_profiles/valve/index_controller",
"component_paths": [
"/input/trigger/value",
"/input/trigger/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"oculus",
{
"profile": "/interaction_profiles/oculus/touch_controller",
"component_paths": [
"/input/trigger/value",
"/input/trigger/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"reverb_g2",
{
"profile": "/interaction_profiles/hp/mixed_reality_controller",
"component_paths": [
"/input/trigger/value",
"/input/trigger/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"simple",
{
"profile": "/interaction_profiles/khr/simple_controller",
"component_paths": [
"/input/select/click",
"/input/select/click",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"vive",
{
"profile": "/interaction_profiles/htc/vive_controller",
"component_paths": [
"/input/trigger/value",
"/input/trigger/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"vive_cosmos",
{
"profile": "/interaction_profiles/htc/vive_cosmos_controller",
"component_paths": [
"/input/trigger/value",
"/input/trigger/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"vive_focus",
{
"profile": "/interaction_profiles/htc/vive_focus3_controller",
"component_paths": [
"/input/trigger/value",
"/input/trigger/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"wmr",
{
"profile": "/interaction_profiles/microsoft/motion_controller",
"component_paths": [
"/input/trigger/value",
"/input/trigger/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
],
},
),
(
"viewfinder_cycle_mode",
{
"type": "FLOAT",
"user_paths": ["/user/hand/left", "/user/hand/right"],
"op": "view3d.vr_location_scouting_viewfinder_cycle_mode",
"op_mode": "PRESS",
"bimanual": "False",
"haptic_name": "haptic",
"haptic_match_user_paths": "True",
"haptic_duration": "0.3",
"haptic_frequency": "3000.0",
"haptic_amplitude": "0.5",
"haptic_mode": "PRESS",
},
{"op_properties": []},
{
"bindings": [
(
"huawei",
{
"profile": "/interaction_profiles/huawei/controller",
"component_paths": [
"/input/trackpad/click",
"/input/trackpad/click",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"index",
{
"profile": "/interaction_profiles/valve/index_controller",
"component_paths": [
"/input/squeeze/force",
"/input/squeeze/force",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"oculus",
{
"profile": "/interaction_profiles/oculus/touch_controller",
"component_paths": [
"/input/squeeze/value",
"/input/squeeze/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"reverb_g2",
{
"profile": "/interaction_profiles/hp/mixed_reality_controller",
"component_paths": [
"/input/squeeze/value",
"/input/squeeze/value",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"simple",
{
"profile": "/interaction_profiles/khr/simple_controller",
"component_paths": [
"/input/menu/click",
"/input/menu/click",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"vive",
{
"profile": "/interaction_profiles/htc/vive_controller",
"component_paths": [
"/input/squeeze/click",
"/input/squeeze/click",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"vive_cosmos",
{
"profile": "/interaction_profiles/htc/vive_cosmos_controller",
"component_paths": [
"/input/squeeze/click",
"/input/squeeze/click",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"vive_focus",
{
"profile": "/interaction_profiles/htc/vive_focus3_controller",
"component_paths": [
"/input/squeeze/click",
"/input/squeeze/click",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
(
"wmr",
{
"profile": "/interaction_profiles/microsoft/motion_controller",
"component_paths": [
"/input/squeeze/click",
"/input/squeeze/click",
],
"threshold": "0.5",
"axis_region": "ANY",
},
),
],
},
),
(
"viewfinder_cycle_action",
{
"type": "FLOAT",
"user_paths": ["/user/hand/left", "/user/hand/right"],
"op": "view3d.vr_location_scouting_viewfinder_cycle_action",
"op_mode": "PRESS",
"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": []},
{
"bindings": [
(
"huawei",
{
"profile": "/interaction_profiles/huawei/controller",
"component_paths": ["/input/trackpad/x", "/input/trackpad/x"],
"threshold": "0.65",
"axis_region": "ANY",
},
),
(
"index",
{
"profile": "/interaction_profiles/valve/index_controller",
"component_paths": ["/input/thumbstick/x", "/input/thumbstick/x"],
"threshold": "0.65",
"axis_region": "ANY",
},
),
(
"oculus",
{
"profile": "/interaction_profiles/oculus/touch_controller",
"component_paths": ["/input/thumbstick/x", "/input/thumbstick/x"],
"threshold": "0.65",
"axis_region": "ANY",
},
),
(
"reverb_g2",
{
"profile": "/interaction_profiles/hp/mixed_reality_controller",
"component_paths": ["/input/thumbstick/x", "/input/thumbstick/x"],
"threshold": "0.65",
"axis_region": "ANY",
},
),
(
"vive",
{
"profile": "/interaction_profiles/htc/vive_controller",
"component_paths": ["/input/trackpad/x", "/input/trackpad/x"],
"threshold": "0.65",
"axis_region": "ANY",
},
),
(
"vive_cosmos",
{
"profile": "/interaction_profiles/htc/vive_cosmos_controller",
"component_paths": ["/input/thumbstick/x", "/input/thumbstick/x"],
"threshold": "0.65",
"axis_region": "ANY",
},
),
(
"vive_focus",
{
"profile": "/interaction_profiles/htc/vive_focus3_controller",
"component_paths": ["/input/thumbstick/x", "/input/thumbstick/x"],
"threshold": "0.65",
"axis_region": "ANY",
},
),
(
"wmr",
{
"profile": "/interaction_profiles/microsoft/motion_controller",
"component_paths": ["/input/thumbstick/x", "/input/thumbstick/x"],
"threshold": "0.65",
"axis_region": "ANY",
},
),
],
},
),
(
"viewfinder_apply_action_up",
{
"type": "FLOAT",
"user_paths": ["/user/hand/left", "/user/hand/right"],
"op": "view3d.vr_location_scouting_viewfinder_apply_action",
"op_mode": "PRESS",
"bimanual": "False",
"haptic_name": "haptic",
"haptic_match_user_paths": "True",
"haptic_duration": "0.3",
"haptic_frequency": "3000.0",
"haptic_amplitude": "0.5",
"haptic_mode": "PRESS",
},
{"op_properties": [("action_up", True)]},
{
"bindings": [
(
"huawei",
{
"profile": "/interaction_profiles/huawei/controller",
"component_paths": [
"/input/home/click",
"/input/home/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"index",
{
"profile": "/interaction_profiles/valve/index_controller",
"component_paths": [
"/input/b/click",
"/input/b/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"oculus",
{
"profile": "/interaction_profiles/oculus/touch_controller",
"component_paths": [
"/input/y/click",
"/input/b/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"reverb_g2",
{
"profile": "/interaction_profiles/hp/mixed_reality_controller",
"component_paths": [
"/input/y/click",
"/input/b/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive",
{
"profile": "/interaction_profiles/htc/vive_controller",
"component_paths": [
"/input/menu/click",
"/input/menu/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive_cosmos",
{
"profile": "/interaction_profiles/htc/vive_cosmos_controller",
"component_paths": [
"/input/y/click",
"/input/b/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive_focus",
{
"profile": "/interaction_profiles/htc/vive_focus3_controller",
"component_paths": [
"/input/y/click",
"/input/b/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"wmr",
{
"profile": "/interaction_profiles/microsoft/motion_controller",
"component_paths": [
"/input/menu/click",
"/input/menu/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
],
},
),
(
"viewfinder_apply_action_down",
{
"type": "FLOAT",
"user_paths": ["/user/hand/left", "/user/hand/right"],
"op": "view3d.vr_location_scouting_viewfinder_apply_action",
"op_mode": "PRESS",
"bimanual": "False",
"haptic_name": "haptic",
"haptic_match_user_paths": "True",
"haptic_duration": "0.3",
"haptic_frequency": "3000.0",
"haptic_amplitude": "0.5",
"haptic_mode": "PRESS",
},
{"op_properties": [("action_up", False)]},
{
"bindings": [
(
"huawei",
{
"profile": "/interaction_profiles/huawei/controller",
"component_paths": [
"/input/back/click",
"/input/back/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"index",
{
"profile": "/interaction_profiles/valve/index_controller",
"component_paths": [
"/input/a/click",
"/input/a/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"oculus",
{
"profile": "/interaction_profiles/oculus/touch_controller",
"component_paths": [
"/input/x/click",
"/input/a/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"reverb_g2",
{
"profile": "/interaction_profiles/hp/mixed_reality_controller",
"component_paths": [
"/input/x/click",
"/input/a/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive",
{
"profile": "/interaction_profiles/htc/vive_controller",
"component_paths": [
"/input/menu/click",
"/input/menu/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive_cosmos",
{
"profile": "/interaction_profiles/htc/vive_cosmos_controller",
"component_paths": [
"/input/x/click",
"/input/a/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive_focus",
{
"profile": "/interaction_profiles/htc/vive_focus3_controller",
"component_paths": [
"/input/x/click",
"/input/a/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"wmr",
{
"profile": "/interaction_profiles/microsoft/motion_controller",
"component_paths": [
"/input/menu/click",
"/input/menu/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
],
},
),
(
"viewfinder_swap_hands",
{
"type": "FLOAT",
"user_paths": ["/user/hand/left", "/user/hand/right"],
"op": "view3d.vr_location_scouting_viewfinder_swap_hands",
"op_mode": "PRESS",
"bimanual": "False",
"haptic_name": "haptic",
"haptic_match_user_paths": "True",
"haptic_duration": "0.3",
"haptic_frequency": "3000.0",
"haptic_amplitude": "0.5",
"haptic_mode": "PRESS",
},
{"op_properties": []},
{
"bindings": [
(
"huawei",
{
"profile": "/interaction_profiles/huawei/controller",
"component_paths": [
"/input/trackpad/click",
"/input/trackpad/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"index",
{
"profile": "/interaction_profiles/valve/index_controller",
"component_paths": [
"/input/thumbstick/click",
"/input/thumbstick/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"oculus",
{
"profile": "/interaction_profiles/oculus/touch_controller",
"component_paths": [
"/input/thumbstick/click",
"/input/thumbstick/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"reverb_g2",
{
"profile": "/interaction_profiles/hp/mixed_reality_controller",
"component_paths": [
"/input/thumbstick/click",
"/input/thumbstick/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive",
{
"profile": "/interaction_profiles/htc/vive_controller",
"component_paths": [
"/input/trackpad/click",
"/input/trackpad/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive_cosmos",
{
"profile": "/interaction_profiles/htc/vive_cosmos_controller",
"component_paths": [
"/input/thumbstick/click",
"/input/thumbstick/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"vive_focus",
{
"profile": "/interaction_profiles/htc/vive_focus3_controller",
"component_paths": [
"/input/thumbstick/click",
"/input/thumbstick/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
(
"wmr",
{
"profile": "/interaction_profiles/microsoft/motion_controller",
"component_paths": [
"/input/thumbstick/click",
"/input/thumbstick/click",
],
"threshold": "0.3",
"axis_region": "ANY",
},
),
],
},
),
(
"haptic",
{
@ -1991,7 +2693,6 @@ actionconfig_data = [
),
]
if __name__ == "__main__":
# Only add keywords that are supported.
from bpy.app import version as blender_version

View file

@ -37,6 +37,12 @@ class VRDefaultActions(Enum):
FLY_TURNLEFT = "fly_turnleft"
FLY_TURNRIGHT = "fly_turnright"
NAV_RESET = "nav_reset"
VIEWFINDER_CAPTURE = "viewfinder_capture"
VIEWFINDER_CYCLE_MODE = "viewfinder_cycle_mode"
VIEWFINDER_CYCLE_ACTION = "viewfinder_cycle_action"
VIEWFINDER_APPLY_ACTION_UP = "viewfinder_apply_action_up"
VIEWFINDER_APPLY_ACTION_DOWN = "viewfinder_apply_action_down"
VIEWFINDER_SWAP_HANDS = "viewfinder_swap_hands"
SWAP_HANDS = "swap_hands"
HAPTIC = "haptic"
HAPTIC_LEFT = "haptic_left"
@ -1147,6 +1153,497 @@ def vr_defaults_create_default(session_state):
'ANY',
'ANY')
# Viewfinder actions.
ami = vr_defaults_action_add(am,
VRDefaultActions.VIEWFINDER_CAPTURE.value,
["/user/hand/left",
"/user/hand/right"],
"view3d.vr_location_scouting_viewfinder_capture",
'PRESS',
False,
"",
False,
0.0,
0.0,
0.0,
'PRESS')
if ami:
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.HUAWEI.value,
VRDefaultActionprofiles.HUAWEI.value,
["/input/trigger/value",
"/input/trigger/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.INDEX.value,
VRDefaultActionprofiles.INDEX.value,
["/input/trigger/value",
"/input/trigger/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.OCULUS.value,
VRDefaultActionprofiles.OCULUS.value,
["/input/trigger/value",
"/input/trigger/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.REVERB_G2.value,
VRDefaultActionprofiles.REVERB_G2.value,
["/input/trigger/value",
"/input/trigger/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.SIMPLE.value,
VRDefaultActionprofiles.SIMPLE.value,
["/input/select/click",
"/input/select/click"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE.value,
VRDefaultActionprofiles.VIVE.value,
["/input/trigger/value",
"/input/trigger/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_COSMOS.value,
VRDefaultActionprofiles.VIVE_COSMOS.value,
["/input/trigger/value",
"/input/trigger/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_FOCUS.value,
VRDefaultActionprofiles.VIVE_FOCUS.value,
["/input/trigger/value",
"/input/trigger/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.WMR.value,
VRDefaultActionprofiles.WMR.value,
["/input/trigger/value",
"/input/trigger/value"],
0.5,
'ANY',
'ANY')
ami = vr_defaults_action_add(am,
VRDefaultActions.VIEWFINDER_CYCLE_MODE.value,
["/user/hand/left",
"/user/hand/right"],
"view3d.vr_location_scouting_viewfinder_cycle_mode",
'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/trackpad/click",
"/input/trackpad/click"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.INDEX.value,
VRDefaultActionprofiles.INDEX.value,
["/input/squeeze/force",
"/input/squeeze/force"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.OCULUS.value,
VRDefaultActionprofiles.OCULUS.value,
["/input/squeeze/value",
"/input/squeeze/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.REVERB_G2.value,
VRDefaultActionprofiles.REVERB_G2.value,
["/input/squeeze/value",
"/input/squeeze/value"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.SIMPLE.value,
VRDefaultActionprofiles.SIMPLE.value,
["/input/menu/click",
"/input/menu/click"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE.value,
VRDefaultActionprofiles.VIVE.value,
["/input/squeeze/click",
"/input/squeeze/click"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_COSMOS.value,
VRDefaultActionprofiles.VIVE_COSMOS.value,
["/input/squeeze/click",
"/input/squeeze/click"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_FOCUS.value,
VRDefaultActionprofiles.VIVE_FOCUS.value,
["/input/squeeze/click",
"/input/squeeze/click"],
0.5,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.WMR.value,
VRDefaultActionprofiles.WMR.value,
["/input/squeeze/click",
"/input/squeeze/click"],
0.5,
'ANY',
'ANY')
ami = vr_defaults_action_add(am,
VRDefaultActions.VIEWFINDER_CYCLE_ACTION.value,
["/user/hand/left",
"/user/hand/right"],
"view3d.vr_location_scouting_viewfinder_cycle_action",
'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/trackpad/x",
"/input/trackpad/x"],
0.65,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.INDEX.value,
VRDefaultActionprofiles.INDEX.value,
["/input/thumbstick/x",
"/input/thumbstick/x"],
0.65,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.OCULUS.value,
VRDefaultActionprofiles.OCULUS.value,
["/input/thumbstick/x",
"/input/thumbstick/x"],
0.65,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.REVERB_G2.value,
VRDefaultActionprofiles.REVERB_G2.value,
["/input/thumbstick/x",
"/input/thumbstick/x"],
0.65,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE.value,
VRDefaultActionprofiles.VIVE.value,
["/input/trackpad/x",
"/input/trackpad/x"],
0.65,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_COSMOS.value,
VRDefaultActionprofiles.VIVE_COSMOS.value,
["/input/thumbstick/x",
"/input/thumbstick/x"],
0.65,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_FOCUS.value,
VRDefaultActionprofiles.VIVE_FOCUS.value,
["/input/thumbstick/x",
"/input/thumbstick/x"],
0.65,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.WMR.value,
VRDefaultActionprofiles.WMR.value,
["/input/thumbstick/x",
"/input/thumbstick/x"],
0.65,
'ANY',
'ANY')
ami = vr_defaults_action_add(am,
VRDefaultActions.VIEWFINDER_APPLY_ACTION_UP.value,
["/user/hand/left",
"/user/hand/right"],
"view3d.vr_location_scouting_viewfinder_apply_action",
'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/home/click",
"/input/home/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_action_add(am,
VRDefaultActions.VIEWFINDER_APPLY_ACTION_DOWN.value,
["/user/hand/left",
"/user/hand/right"],
"view3d.vr_location_scouting_viewfinder_apply_action",
'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/a/click",
"/input/a/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.OCULUS.value,
VRDefaultActionprofiles.OCULUS.value,
["/input/x/click",
"/input/a/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.REVERB_G2.value,
VRDefaultActionprofiles.REVERB_G2.value,
["/input/x/click",
"/input/a/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/x/click",
"/input/a/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_FOCUS.value,
VRDefaultActionprofiles.VIVE_FOCUS.value,
["/input/x/click",
"/input/a/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_action_add(am,
VRDefaultActions.VIEWFINDER_SWAP_HANDS.value,
["/user/hand/left",
"/user/hand/right"],
"view3d.vr_location_scouting_viewfinder_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/trackpad/click",
"/input/trackpad/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.INDEX.value,
VRDefaultActionprofiles.INDEX.value,
["/input/thumbstick/click",
"/input/thumbstick/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.OCULUS.value,
VRDefaultActionprofiles.OCULUS.value,
["/input/thumbstick/click",
"/input/thumbstick/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.REVERB_G2.value,
VRDefaultActionprofiles.REVERB_G2.value,
["/input/thumbstick/click",
"/input/thumbstick/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE.value,
VRDefaultActionprofiles.VIVE.value,
["/input/trackpad/click",
"/input/trackpad/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_COSMOS.value,
VRDefaultActionprofiles.VIVE_COSMOS.value,
["/input/thumbstick/click",
"/input/thumbstick/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.VIVE_FOCUS.value,
VRDefaultActionprofiles.VIVE_FOCUS.value,
["/input/thumbstick/click",
"/input/thumbstick/click"],
0.3,
'ANY',
'ANY')
vr_defaults_actionbinding_add(ami,
VRDefaultActionbindings.WMR.value,
VRDefaultActionprofiles.WMR.value,
["/input/thumbstick/click",
"/input/thumbstick/click"],
0.3,
'ANY',
'ANY')
ami = vr_defaults_action_add(am,
VRDefaultActions.SWAP_HANDS.value,
["/user/hand/left",

View file

@ -26,11 +26,14 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../s
from space_view3d import VIEW3D_PT_object_type_visibility
# Session.
class VIEW3D_PT_vr_session(Panel):
class VRButtonsPanel:
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "VR"
# Session.
class VIEW3D_PT_vr_session(VRButtonsPanel, Panel):
bl_label = "VR Session"
def draw(self, context):
@ -61,10 +64,7 @@ class VIEW3D_PT_vr_session(Panel):
# View.
class VIEW3D_PT_vr_session_view(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "VR"
class VIEW3D_PT_vr_session_view(VRButtonsPanel, Panel):
bl_label = "View"
def draw(self, context):
@ -112,6 +112,104 @@ class VIEW3D_PT_vr_session_view_object_type_visibility(VIEW3D_PT_object_type_vis
self.draw_ex(context, session_settings, False) # Pass session settings instead of 3D view.
# Location Scouting.
class VIEW3D_UL_vr_captures(UIList):
def draw_item(self, context, layout, _data, item, icon, _active_data, _active_propname, index):
capture = item
layout.emboss = 'NONE'
layout.label(icon='OUTLINER_OB_CAMERA')
layout.prop(capture, "name", text="")
class VIEW3D_PT_vr_location_scouting(VRButtonsPanel, Panel):
bl_label = "Location Scouting"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
pass
class VIEW3D_PT_vr_location_scouting_captures(VRButtonsPanel, Panel):
bl_label = "Captured Shots"
bl_parent_id = "VIEW3D_PT_vr_location_scouting"
def draw(self, context):
layout = self.layout
scene = context.scene
row = layout.row()
row.template_list("VIEW3D_UL_vr_captures", "", scene, "vr_captures", scene, "vr_captures_selected", rows=5)
col = row.column(align=True)
col.operator("view3d.vr_location_scouting_remove_capture", icon='REMOVE', text="")
col.separator()
col.operator("view3d.vr_location_scouting_add_camera_from_capture", icon='OUTLINER_OB_CAMERA', text="")
col.operator("view3d.vr_location_scouting_add_marker_from_capture", icon='MARKER', text="")
col.separator()
col.operator("view3d.vr_location_scouting_browse_captures", icon='TRIA_UP', text="").backward = True
col.operator("view3d.vr_location_scouting_browse_captures", icon='TRIA_DOWN', text="").backward = False
is_reviewing = context.window_manager.vr_capture_review_running
capture_review_text = "Review VR Captures" if not is_reviewing else "Exit Review"
capture_review_icon = 'HIDE_OFF' if not is_reviewing else 'CANCEL'
layout.operator(
"view3d.vr_location_scouting_capture_review",
text=capture_review_text,
icon=capture_review_icon,
depress=is_reviewing)
class VIEW3D_PT_vr_location_scouting_viewfinder(VRButtonsPanel, Panel):
bl_label = "VR Viewfinder"
bl_parent_id = "VIEW3D_PT_vr_location_scouting"
def draw_header(self, context):
layout = self.layout
session_settings = context.window_manager.xr_session_settings
layout.prop(session_settings, "viewfinder_enabled", text="")
def draw(self, context):
layout = self.layout
session_settings = context.window_manager.xr_session_settings
layout.enabled = session_settings.viewfinder_enabled
session_settings = context.window_manager.xr_session_settings
layout.use_property_split = True
layout.prop(session_settings, "viewfinder_hand", text="Hand", expand=True)
layout.prop(session_settings, "viewfinder_scale", text="Scale")
col = layout.column(align=True, heading="Display")
col.prop(session_settings, "viewfinder_crosshair_enabled", text="Crosshair")
class VIEW3D_PT_vr_location_scouting_viewfinder_passepartout(VRButtonsPanel, Panel):
bl_label = "Passepartout"
bl_parent_id = "VIEW3D_PT_vr_location_scouting_viewfinder"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
session_settings = context.window_manager.xr_session_settings
layout.enabled = session_settings.viewfinder_enabled
session_settings = context.window_manager.xr_session_settings
layout.use_property_split = True
layout.prop(session_settings, "viewfinder_passepartout_overscan", text="Overscan")
layout.prop(session_settings, "viewfinder_passepartout_opacity", text="Opacity")
# Landmarks.
class VIEW3D_MT_vr_landmark_menu(Menu):
bl_label = "Landmark Controls"
@ -146,10 +244,7 @@ class VIEW3D_UL_vr_landmarks(UIList):
props.index = index
class VIEW3D_PT_vr_landmarks(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "VR"
class VIEW3D_PT_vr_landmarks(VRButtonsPanel, Panel):
bl_label = "Landmarks"
bl_options = {'DEFAULT_CLOSED'}
@ -189,10 +284,7 @@ class VIEW3D_PT_vr_landmarks(Panel):
# Actions.
class VIEW3D_PT_vr_actionmaps(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "VR"
class VIEW3D_PT_vr_actionmaps(VRButtonsPanel, Panel):
bl_label = "Action Maps"
bl_options = {'DEFAULT_CLOSED'}
@ -214,17 +306,13 @@ class VIEW3D_PT_vr_actionmaps(Panel):
# Viewport feedback.
class VIEW3D_PT_vr_viewport_feedback(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "VR"
class VIEW3D_PT_vr_viewport_feedback(VRButtonsPanel, Panel):
bl_label = "Viewport Feedback"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
view3d = context.space_data
session_settings = context.window_manager.xr_session_settings
col = layout.column(align=True)
col.label(icon='ERROR', text="Note:")
@ -236,14 +324,12 @@ class VIEW3D_PT_vr_viewport_feedback(Panel):
layout.prop(view3d.shading, "vr_show_virtual_camera")
layout.prop(view3d.shading, "vr_show_controllers")
layout.prop(view3d.shading, "vr_show_landmarks")
layout.prop(view3d.shading, "vr_show_captures")
layout.prop(view3d, "mirror_xr_session")
# Info.
class VIEW3D_PT_vr_info(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "VR"
class VIEW3D_PT_vr_info(VRButtonsPanel, Panel):
bl_label = "VR Info"
@classmethod
@ -261,11 +347,16 @@ classes = (
VIEW3D_PT_vr_session,
VIEW3D_PT_vr_session_view,
VIEW3D_PT_vr_session_view_object_type_visibility,
VIEW3D_PT_vr_location_scouting,
VIEW3D_PT_vr_location_scouting_captures,
VIEW3D_PT_vr_location_scouting_viewfinder,
VIEW3D_PT_vr_location_scouting_viewfinder_passepartout,
VIEW3D_PT_vr_landmarks,
VIEW3D_PT_vr_actionmaps,
VIEW3D_PT_vr_viewport_feedback,
VIEW3D_UL_vr_landmarks,
VIEW3D_UL_vr_captures,
VIEW3D_MT_vr_landmark_menu,
)
@ -277,13 +368,20 @@ def register():
# View3DShading is the only per 3D-View struct with custom property
# support, so "abusing" that to get a per 3D-View option.
bpy.types.View3DShading.vr_show_virtual_camera = bpy.props.BoolProperty(
name="Show VR Camera"
name="Show VR Camera",
default=False
)
bpy.types.View3DShading.vr_show_controllers = bpy.props.BoolProperty(
name="Show VR Controllers"
name="Show VR Controllers",
default=False
)
bpy.types.View3DShading.vr_show_landmarks = bpy.props.BoolProperty(
name="Show Landmarks"
name="Show Landmarks",
default=False
)
bpy.types.View3DShading.vr_show_captures = bpy.props.BoolProperty(
name="Show Location Scouting Captures",
default=True
)
@ -294,3 +392,4 @@ def unregister():
del bpy.types.View3DShading.vr_show_virtual_camera
del bpy.types.View3DShading.vr_show_controllers
del bpy.types.View3DShading.vr_show_landmarks
del bpy.types.View3DShading.vr_show_captures

View file

@ -18,7 +18,7 @@ from bpy.types import (
)
import math
from math import radians
from mathutils import Euler, Matrix, Quaternion, Vector
from mathutils import Color, Euler, Matrix, Quaternion, Vector
# Landmarks.
@ -275,22 +275,567 @@ class VIEW3D_OT_vr_landmark_activate(Operator):
return {'FINISHED'}
# Location Scouting Viewfinder
def viewfinder_camera_gizmo_view3d_redraw_workaround():
# Workaround: After capturing or deleting a shot from the VR viewfinder, tag all View3D areas in the current context
# window (parent XR window) for redraw to display the newly created/deleted captures.
# The alternative to this is to give the capture camera Gizmo (VIEW3D_GGT_vr_captures) the VR_REDRAWS
# option, however this makes it constantly redraw, causing performances to drop.
window = bpy.context.window
areas = [area for area in window.screen.areas if area.type == 'VIEW_3D']
for area in areas:
view3d_region = [region for region in area.regions if region.type == 'WINDOW'][0]
with bpy.context.temp_override(
window=window,
area=area,
region=view3d_region,
screen=window.screen
):
bpy.context.region.tag_redraw()
def xr_event_match_viewfinder_hand(xr_event, xr_settings):
# Check if the current XR event matches with the Viewfinder hand. Equivalent to the internal
# wm_xr_operator_event_match_viewfinder_hand C++ function.
hand_user_path_map = {
'LEFT': "/user/hand/left",
'RIGHT': "/user/hand/right"
}
return xr_event.user_path == hand_user_path_map[xr_settings.viewfinder_hand]
class VIEW3D_OT_vr_location_scouting_viewfinder_capture(Operator):
bl_idname = "view3d.vr_location_scouting_viewfinder_capture"
bl_label = "Viewfinder Capture"
bl_description = "Create a VR Capture from the Viewfinder pose and mark it as selected"
bl_options = {'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context):
session_is_running = bpy.types.XrSessionState.is_running(context)
xr_settings = context.window_manager.xr_session_settings
xr_viewfinder = context.window_manager.xr_session_state.viewfinder
viewfinder_enabled = xr_settings.viewfinder_enabled
viewfinder_in_live_mode = xr_viewfinder.active_mode == "LIVE"
return session_is_running and viewfinder_enabled and viewfinder_in_live_mode
def execute(self, context):
scene = context.scene
captures = scene.vr_captures
wm = context.window_manager
xr_viewfinder = wm.xr_session_state.viewfinder
# Returns the first available name in the style (Base_001, Base_002, Base_003, etc...).
def unique_name(col, base: str) -> str:
existing_indexes = set()
for item in col:
name = item.name
if name.startswith(base) and len(name) == len(base) + 4:
suffix = name[len(base) + 1:]
if suffix.isdigit():
existing_indexes.add(int(suffix))
idx = 1
while idx in existing_indexes:
idx += 1
return f"{base}_{idx:03d}"
capture = captures.add()
captures[-1].name = unique_name(captures, "Capture")
scene.vr_captures_selected = len(captures) - 1
capture.location = xr_viewfinder.location
capture.orientation = xr_viewfinder.orientation
capture.lens_focal = xr_viewfinder.capture_lens_focal
capture.dof_enabled = xr_viewfinder.capture_dof_enabled
capture.dof_distance = xr_viewfinder.capture_dof_distance
capture.dof_fstop = xr_viewfinder.capture_dof_fstop
xr_viewfinder.trigger_flash()
viewfinder_camera_gizmo_view3d_redraw_workaround()
return {'FINISHED'}
def invoke(self, context, event):
xr_event = event.xr
xr_settings = context.window_manager.xr_session_settings
if not xr_event_match_viewfinder_hand(xr_event, xr_settings):
return {'CANCELLED'}
return self.execute(context)
class VIEW3D_OT_vr_location_scouting_viewfinder_apply_action(Operator):
bl_idname = "view3d.vr_location_scouting_viewfinder_apply_action"
bl_label = "Viewfinder Apply Action"
bl_description = "Apply the currently selected Viewfinder action (Zoom Control/Playback selection for now)"
bl_options = {'INTERNAL'}
# Differentiate between an up and down action (two possible buttons).
action_up: bpy.props.BoolProperty(
name="Is Up Action",
options={'HIDDEN'},
)
@staticmethod
def get_next_in_map(current, map_, up_dir) -> int:
# Find the closest map idx to the `current` value.
diff_list = [abs(elem - current) for elem in map_]
current_idx = diff_list.index(min(diff_list))
# Find the next element going up or down, clamping at bounds.
if up_dir:
# Zoom in
next_idx = min(current_idx + 1, len(map_) - 1)
else:
# Zoom out
next_idx = max(current_idx - 1, 0)
return next_idx
@staticmethod
def focus_distance_raycast(context, view_origin, view_quat):
scene = context.scene
depsgraph = context.evaluated_depsgraph_get()
direction = Vector((0.0, 0.0, -1.0))
world_dir = view_quat @ direction
world_dir.normalize()
hit_success, hit_location, _, _, _, _ = scene.ray_cast(depsgraph, view_origin, world_dir)
if hit_success:
return (hit_location - view_origin).length
return None
@classmethod
def poll(cls, context):
session_is_running = bpy.types.XrSessionState.is_running(context)
has_scene_camera = context.scene.camera is not None
viewfinder_enabled = context.window_manager.xr_session_settings.viewfinder_enabled
return session_is_running and has_scene_camera and viewfinder_enabled
def execute(self, context):
wm = context.window_manager
scene = context.scene
xr_viewfinder = wm.xr_session_state.viewfinder
captures = scene.vr_captures
if xr_viewfinder.active_mode == 'LIVE':
focal_map = (18, 20, 24, 28, 35, 50, 70, 85, 100, 135, 200, 300)
fstop_map = (0.1, 0.2, 0.4, 0.8, 1, 1.2, 1.4, 1.7, 2, 2.4, 2.8, 3.3,
4, 4.8, 5.6, 6.7, 8, 9.5, 11, 13, 16, 19, 22, 27, 32)
match xr_viewfinder.active_action_live:
# View Zoom Control.
case 'LENS':
current_focal = xr_viewfinder.capture_lens_focal
new_idx = self.get_next_in_map(current_focal, focal_map, self.action_up)
xr_viewfinder.capture_lens_focal = focal_map[new_idx]
return {'FINISHED'}
# Toggle DoF on/off.
case 'DOF':
xr_viewfinder.capture_dof_enabled = not xr_viewfinder.capture_dof_enabled
return {'FINISHED'}
# Focus distance control (ray-cast autofocus).
case 'FOCUS':
raycast_hit = self.focus_distance_raycast(context,
xr_viewfinder.location,
xr_viewfinder.orientation)
if raycast_hit is not None:
xr_viewfinder.capture_dof_distance = raycast_hit
xr_viewfinder.trigger_focus_indicator(raycast_hit is not None)
return {'FINISHED'}
# F-Stop control.
case 'APERTURE':
current_fstop = xr_viewfinder.capture_dof_fstop
new_idx = self.get_next_in_map(current_fstop, fstop_map, self.action_up)
xr_viewfinder.capture_dof_fstop = fstop_map[new_idx]
return {'FINISHED'}
if xr_viewfinder.active_mode == 'PLAYBACK':
# Playblack control.
if len(captures) == 0:
return {'FINISHED'}
match xr_viewfinder.active_action_playback:
# Browse shots left/right.
case 'BROWSE':
incr = 1 if self.action_up else -1
scene.vr_captures_selected = (scene.vr_captures_selected + incr) % len(captures)
return {'FINISHED'}
# Preview the selected capture in space toggle.
case 'PREVIEW':
xr_viewfinder.playback_show_active_capture_in_space_enabled = not xr_viewfinder.playback_show_active_capture_in_space_enabled
return {'FINISHED'}
# Delete, switch to Confirm mode to confirm capture deletion.
case 'DELETE':
xr_viewfinder.active_mode = 'CONFIRM'
# Set base action to Cancel, user has to explicitly switch to the Confirm action.
xr_viewfinder.active_action_confirm = 'CANCEL'
return {'FINISHED'}
if xr_viewfinder.active_mode == 'CONFIRM':
# Confirm mode, currently only used for capture deletion, and only
# accessible from the 'DELETE' Playback action.
match xr_viewfinder.active_action_confirm:
# Cancel, simply switch back to Playback mode.
case 'CANCEL':
xr_viewfinder.active_mode = 'PLAYBACK'
return {'FINISHED'}
# Confirm, delete the selected capture, switch back to Playback mode.
case 'CONFIRM':
captures.remove(scene.vr_captures_selected)
if scene.vr_captures_selected > 0:
scene.vr_captures_selected -= 1
viewfinder_camera_gizmo_view3d_redraw_workaround()
xr_viewfinder.active_mode = 'PLAYBACK'
return {'FINISHED'}
return {'CANCELLED'}
def invoke(self, context, event):
xr_event = event.xr
xr_settings = context.window_manager.xr_session_settings
if not xr_event_match_viewfinder_hand(xr_event, xr_settings):
return {'CANCELLED'}
return self.execute(context)
class VIEW3D_OT_vr_location_scouting_viewfinder_cycle_mode(Operator):
bl_idname = "view3d.vr_location_scouting_viewfinder_cycle_mode"
bl_label = "Viewfinder Cycle Mode"
bl_description = "Cycle the currently active Viewfinder mode"
bl_options = {'INTERNAL'}
def execute(self, context):
xr_viewfinder = context.window_manager.xr_session_state.viewfinder
# Limit cycling to Live and Playback, Confirm being an internal mode used for capture deletion.
enum_values = ['LIVE', 'PLAYBACK']
if xr_viewfinder.active_mode not in enum_values:
return {'CANCELLED'}
current_mode_idx = enum_values.index(xr_viewfinder.active_mode)
xr_viewfinder.active_mode = enum_values[(current_mode_idx + 1) % len(enum_values)]
return {'FINISHED'}
def invoke(self, context, event):
xr_event = event.xr
xr_settings = context.window_manager.xr_session_settings
if not xr_event_match_viewfinder_hand(xr_event, xr_settings):
return {'CANCELLED'}
return self.execute(context)
class VIEW3D_OT_vr_location_scouting_viewfinder_cycle_action(Operator):
bl_idname = "view3d.vr_location_scouting_viewfinder_cycle_action"
bl_label = "Viewfinder Cycle Action"
bl_description = "Cycle the currently active Viewfinder action left or right"
bl_options = {'INTERNAL'}
cycle_left: bpy.props.BoolProperty(
name="Cycle Left",
default=False,
options={'HIDDEN', 'SKIP_SAVE'},
)
def execute(self, context):
xr_viewfinder = context.window_manager.xr_session_state.viewfinder
increment = -1 if self.cycle_left else 1
match xr_viewfinder.active_mode:
case 'LIVE':
action_rna_prop = xr_viewfinder.rna_type.properties["active_action_live"]
enum_keys = action_rna_prop.enum_items.keys()
current_action_idx = enum_keys.index(xr_viewfinder.active_action_live)
# Special case: only allow cycling to non-DoF action if DoF is not enabled
enum_length = len(enum_keys) if xr_viewfinder.capture_dof_enabled else (enum_keys.index('DOF') + 1)
new_action_idx = (current_action_idx + increment) % enum_length
xr_viewfinder.active_action_live = enum_keys[new_action_idx]
case 'PLAYBACK':
action_rna_prop = xr_viewfinder.rna_type.properties["active_action_playback"]
enum_keys = action_rna_prop.enum_items.keys()
current_action_idx = enum_keys.index(xr_viewfinder.active_action_playback)
new_action_idx = (current_action_idx + increment) % len(enum_keys)
xr_viewfinder.active_action_playback = enum_keys[new_action_idx]
case 'CONFIRM':
action_rna_prop = xr_viewfinder.rna_type.properties["active_action_confirm"]
enum_keys = action_rna_prop.enum_items.keys()
current_action_idx = enum_keys.index(xr_viewfinder.active_action_confirm)
new_action_idx = (current_action_idx + increment) % len(enum_keys)
xr_viewfinder.active_action_confirm = enum_keys[new_action_idx]
return {'FINISHED'}
def invoke(self, context, event):
xr_event = event.xr
xr_settings = context.window_manager.xr_session_settings
if not xr_event_match_viewfinder_hand(xr_event, xr_settings):
return {'CANCELLED'}
axis_value = xr_event.state[0]
self.cycle_left = axis_value <= 0
return self.execute(context)
class VIEW3D_OT_vr_location_scouting_viewfinder_swap_hands(Operator):
bl_idname = "view3d.vr_location_scouting_viewfinder_swap_hands"
bl_label = "Viewfinder Swap Hands"
bl_description = "Swap user hand used to hold the Viewfinder"
bl_options = {'INTERNAL'}
def execute(self, context):
xr_viewfinder = context.window_manager.xr_session_state.viewfinder
xr_settings = context.window_manager.xr_session_settings
viewfinder_hand_rna_prop = xr_settings.rna_type.properties['viewfinder_hand']
enum_values = viewfinder_hand_rna_prop.enum_items.keys()
current_hand_idx = enum_values.index(xr_settings.viewfinder_hand)
xr_settings.viewfinder_hand = enum_values[(current_hand_idx + 1) % len(enum_values)]
xr_viewfinder.reset_view_smoothing()
return {'FINISHED'}
def invoke(self, context, _event):
xr_settings = context.window_manager.xr_session_settings
if not xr_settings.viewfinder_enabled:
return {'CANCELLED'}
return self.execute(context)
# Location Scouting Captures
def capture_camera_name(capture):
return data_("Camera") + "_" + capture.name
def add_camera_from_capture(scene, capture):
cam = bpy.data.cameras.new(capture_camera_name(capture))
new_cam = bpy.data.objects.new(capture_camera_name(capture), cam)
scene.collection.objects.link(new_cam)
new_cam.location = capture.location
new_cam.rotation_mode = 'QUATERNION'
new_cam.rotation_quaternion = capture.orientation
new_cam.rotation_mode = 'XYZ'
new_cam.data.lens = capture.lens_focal
new_cam.data.dof.use_dof = capture.dof_enabled
new_cam.data.dof.focus_distance = capture.dof_distance
new_cam.data.dof.aperture_fstop = capture.dof_fstop
return new_cam
class VIEW3D_OT_vr_location_scouting_add_camera_from_capture(Operator):
bl_idname = "view3d.vr_location_scouting_add_camera_from_capture"
bl_label = "Add Camera from VR Capture"
bl_description = "Create a new Camera Object from the selected VR Capture"
bl_options = {'UNDO', 'REGISTER'}
@classmethod
def poll(cls, context):
return len(context.scene.vr_captures) > 0
def execute(self, context):
scene = context.scene
capture = properties.VRCapture.get_selected_capture(context)
add_camera_from_capture(scene, capture)
return {'FINISHED'}
class VIEW3D_OT_vr_location_scouting_add_marker_from_capture(Operator):
bl_idname = "view3d.vr_location_scouting_add_marker_from_capture"
bl_label = "Add Camera Marker from VR Capture"
bl_description = "Create a new Camera bound to a Marker from the selected VR Capture at the current frame"
bl_options = {'UNDO', 'REGISTER'}
@classmethod
def poll(cls, context):
return len(context.scene.vr_captures) > 0
def execute(self, context):
scene = context.scene
sel_capture = properties.VRCapture.get_selected_capture(context)
# Check if the selected capture camera already exist, if not create it.
capture_cam = scene.objects.get(capture_camera_name(sel_capture))
def cam_matches_capture(camera, capture):
if camera is None:
return False
return camera.location == capture.location and camera.rotation_quaternion == capture.orientation
if not cam_matches_capture(capture_cam, sel_capture):
capture_cam = add_camera_from_capture(scene, sel_capture)
marker = scene.timeline_markers.new(capture_cam.name, frame=scene.frame_current)
marker.camera = capture_cam
return {'FINISHED'}
class VIEW3D_OT_vr_location_scouting_active_camera_to_capture(Operator):
bl_idname = "view3d.vr_location_scouting_active_camera_to_capture"
bl_label = "Set Active Camera from VR Capture"
bl_description = "Set the active Scene Camera settings from the selected VR Capture"
bl_options = {'UNDO', 'REGISTER'}
@classmethod
def poll(cls, context):
return len(context.scene.vr_captures) > 0
def execute(self, context):
capture = properties.VRCapture.get_selected_capture(context)
cam = context.scene.camera
cam.location = capture.location
cam.rotation_mode = 'QUATERNION'
cam.rotation_quaternion = capture.orientation
cam.rotation_mode = 'XYZ'
cam.data.shift_x = 0
cam.data.shift_y = 0
cam.data.sensor_fit = 'AUTO'
cam.data.sensor_width = 36
cam.data.lens = capture.lens_focal
cam.data.dof.use_dof = capture.dof_enabled
cam.data.dof.focus_distance = capture.dof_distance
cam.data.dof.aperture_fstop = capture.dof_fstop
return {'FINISHED'}
class VIEW3D_OT_vr_location_scouting_remove_capture(Operator):
bl_idname = "view3d.vr_location_scouting_remove_capture"
bl_label = "Remove Location Scouting Capture"
bl_description = "Remove the selected Location Scouting Capture"
bl_options = {'UNDO', 'REGISTER'}
@classmethod
def poll(cls, context):
return len(context.scene.vr_captures) > 0
def execute(self, context):
scene = context.scene
captures = scene.vr_captures
capture_selected_idx = scene.vr_captures_selected
captures.remove(capture_selected_idx)
if scene.vr_captures_selected > 0:
scene.vr_captures_selected -= 1
viewfinder_camera_gizmo_view3d_redraw_workaround()
return {'FINISHED'}
class VIEW3D_OT_vr_location_scouting_browse_captures(Operator):
bl_idname = "view3d.vr_location_scouting_browse_captures"
bl_label = "Browse Location Scouting Captures"
bl_description = "Browse Location Scouting Captures forward or backward"
backward: bpy.props.BoolProperty(
name="Browse Backward",
default=False,
options={'HIDDEN', 'SKIP_SAVE'},
)
@classmethod
def poll(cls, context):
return len(context.scene.vr_captures) > 0
def execute(self, context):
scene = context.scene
incr = -1 if self.backward else 1
scene.vr_captures_selected = (scene.vr_captures_selected + incr) % len(scene.vr_captures)
viewfinder_camera_gizmo_view3d_redraw_workaround()
return {'FINISHED'}
# Gizmos.
class VIEW3D_GT_vr_camera_cone(Gizmo):
bl_idname = "VIEW_3D_GT_vr_camera_cone"
aspect = 1.0, 1.0
focal = 1.0
def draw(self, context):
if not hasattr(self, "frame_shape"):
aspect = self.aspect
ui_scale = context.preferences.view.ui_scale
aspect = tuple(a / ui_scale for a in self.aspect)
focal = self.focal / ui_scale
frame_shape_verts = (
(-aspect[0], -aspect[1], -1.0),
(aspect[0], -aspect[1], -1.0),
(aspect[0], aspect[1], -1.0),
(-aspect[0], aspect[1], -1.0),
(-aspect[0], -aspect[1], -1.0),
(-aspect[0], -aspect[1], -focal),
(aspect[0], -aspect[1], -focal),
(aspect[0], aspect[1], -focal),
(-aspect[0], aspect[1], -focal),
(-aspect[0], -aspect[1], -focal),
)
lines_shape_verts = (
(0.0, 0.0, 0.0),
@ -308,10 +853,17 @@ class VIEW3D_GT_vr_camera_cone(Gizmo):
self.lines_shape = self.new_custom_shape(
'LINES', lines_shape_verts)
# Ensure correct GL state (otherwise other gizmos might mess that up)
gpu.state.line_width_set(1.0)
gpu.state.blend_set('ALPHA')
line_width = 2
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
shader.uniform_float("lineWidth", line_width * gpu.state.line_width_get())
# Override default shader given by new_custom_shape()
self.frame_shape = (self.frame_shape[0], shader)
self.lines_shape = (self.lines_shape[0], shader)
gpu.state.blend_set('ALPHA')
self.draw_custom_shape(self.frame_shape)
self.draw_custom_shape(self.lines_shape)
@ -510,6 +1062,77 @@ class VIEW3D_GGT_vr_landmarks(GizmoGroup):
self.gizmo.matrix_basis = lm_mat
class VIEW3D_GGT_vr_captures(GizmoGroup):
bl_idname = "VIEW3D_GGT_vr_captures"
bl_label = "VR Location Scouting Captures Indicators"
bl_space_type = 'VIEW_3D'
bl_region_type = 'WINDOW'
bl_options = {'3D', 'DEPTH_3D', 'PERSISTENT', 'SCALE'}
@staticmethod
def compute_aspect(render_settings) -> tuple[float, float]:
render_x = render_settings.resolution_x * render_settings.pixel_aspect_x
render_y = render_settings.resolution_y * render_settings.pixel_aspect_y
aspect_x = render_x / render_y if render_x < render_y else 1
aspect_y = render_y / render_x if render_x > render_y else 1
# Base aspect to match native Blender Camera Gizmo (using Auto Sensor Fit).
base_aspect = 1 / 4
return aspect_x * base_aspect, aspect_y * base_aspect
@staticmethod
def get_selection_color(context, is_active_capture) -> tuple[float, float, float]:
selection_color = Color((0.25, 0.81, 1.0))
# Shift the hue of the base theme selection color, decrease its saturation/value further for inactive captures.
if not is_active_capture:
selection_color.s += 0.2
selection_color.v -= 0.5
return selection_color[:3]
@staticmethod
def get_transform_matrix(capture):
rot_mat = Matrix.Identity(3)
rot_mat.rotate(capture.orientation)
rot_mat.resize_4x4()
loc_mat = Matrix.Translation(capture.location)
return loc_mat @ rot_mat
@classmethod
def poll(cls, context):
view3d = context.space_data
return view3d.shading.vr_show_captures
def setup(self, context):
pass
def draw_prepare(self, context):
for g in self.gizmos:
self.gizmos.remove(g)
scene = context.scene
for idx, capture in enumerate(scene.vr_captures):
gizmo = self.gizmos.new(VIEW3D_GT_vr_camera_cone.bl_idname)
gizmo.aspect = self.compute_aspect(scene.render)
sensor_fit_fac = 36 * 2 # Twice the Blender default Camera sensor fit value (36mm).
gizmo.focal = capture.lens_focal / sensor_fit_fac
is_active_capture = (idx == scene.vr_captures_selected)
color = self.get_selection_color(context, is_active_capture)
gizmo.color = color
gizmo.color_highlight = color
gizmo.alpha = 1.0
gizmo.matrix_basis = self.get_transform_matrix(capture)
self.gizmo = gizmo
classes = (
VIEW3D_OT_vr_landmark_add,
VIEW3D_OT_vr_landmark_remove,
@ -522,12 +1145,24 @@ classes = (
VIEW3D_OT_cursor_to_vr_landmark,
VIEW3D_OT_update_vr_landmark,
VIEW3D_OT_vr_location_scouting_viewfinder_capture,
VIEW3D_OT_vr_location_scouting_viewfinder_apply_action,
VIEW3D_OT_vr_location_scouting_add_camera_from_capture,
VIEW3D_OT_vr_location_scouting_add_marker_from_capture,
VIEW3D_OT_vr_location_scouting_active_camera_to_capture,
VIEW3D_OT_vr_location_scouting_remove_capture,
VIEW3D_OT_vr_location_scouting_browse_captures,
VIEW3D_OT_vr_location_scouting_viewfinder_cycle_mode,
VIEW3D_OT_vr_location_scouting_viewfinder_cycle_action,
VIEW3D_OT_vr_location_scouting_viewfinder_swap_hands,
VIEW3D_GT_vr_camera_cone,
VIEW3D_GT_vr_controller_grip,
VIEW3D_GT_vr_controller_aim,
VIEW3D_GGT_vr_viewer_pose,
VIEW3D_GGT_vr_controller_poses,
VIEW3D_GGT_vr_landmarks,
VIEW3D_GGT_vr_captures,
)

View file

@ -194,8 +194,49 @@ class VRLandmark(PropertyGroup):
)
class VRCapture(PropertyGroup):
# Note: This PropertyGroup needs to be kept in sync with the internal XrLocationScoutingCapture struct
# and get_active_location_scouting_capture() function.
name: bpy.props.StringProperty(
name="VR Capture",
default="Capture"
)
location: bpy.props.FloatVectorProperty(
name="Capture Location",
subtype='TRANSLATION',
)
orientation: bpy.props.FloatVectorProperty(
name="Capture Orientation",
size=4,
subtype='QUATERNION',
)
lens_focal: bpy.props.FloatProperty(
name="Capture Focal Length",
)
dof_enabled: bpy.props.BoolProperty(
name="Capture DoF Enabled",
)
dof_distance: bpy.props.FloatProperty(
name="Capture DoF Focus Distance",
)
dof_fstop: bpy.props.FloatProperty(
name="Capture DoF Aperture F-Stop",
)
@staticmethod
def get_selected_capture(context):
scene = context.scene
captures = scene.vr_captures
return (
None if (len(captures) <
1) else captures[scene.vr_captures_selected]
)
classes = (
VRLandmark,
VRCapture,
)
@ -214,6 +255,19 @@ def register():
update=vr_landmark_active_update,
)
bpy.types.Scene.vr_captures = bpy.props.CollectionProperty(
name="Location Scouting Captures",
type=VRCapture,
)
bpy.types.Scene.vr_captures_selected = bpy.props.IntProperty(
name="Selected Capture"
)
bpy.types.WindowManager.vr_capture_review_running = bpy.props.BoolProperty(
name="Capture Review Modal Operator Running",
default=False
)
bpy.app.handlers.load_post.append(vr_ensure_default_landmark)
@ -225,4 +279,9 @@ def unregister():
del bpy.types.Scene.vr_landmarks_selected
del bpy.types.Scene.vr_landmarks_active
del bpy.types.Scene.vr_captures
del bpy.types.Scene.vr_captures_selected
del bpy.types.WindowManager.vr_capture_review_running
bpy.app.handlers.load_post.remove(vr_ensure_default_landmark)

View file

@ -126,6 +126,7 @@ _km_hierarchy = [
('View3D Move Modal', 'EMPTY', 'WINDOW', []),
('View3D Zoom Modal', 'EMPTY', 'WINDOW', []),
('View3D Dolly Modal', 'EMPTY', 'WINDOW', []),
('View3D VR Location Scouting Capture Review Modal', 'EMPTY', 'WINDOW', []),
# toolbar and properties
('3D View Generic', 'VIEW_3D', 'WINDOW', [

View file

@ -6709,6 +6709,28 @@ def km_view3d_dolly_modal(_params):
return keymap
def km_view3d_location_scouting_capture_review_modal(_params):
items = []
keymap = (
"View3D VR Location Scouting Capture Review Modal",
{"space_type": 'EMPTY', "region_type": 'WINDOW', "modal": True},
{"items": items},
)
items.extend([
("EXIT", {"type": 'ESC', "value": 'PRESS', "any": True}, None),
("EXIT", {"type": 'RIGHTMOUSE', "value": 'PRESS', "any": True}, None),
("PREVIOUS", {"type": 'UP_ARROW', "value": 'PRESS'}, None),
("NEXT", {"type": 'DOWN_ARROW', "value": 'PRESS'}, None),
("ADD_CAMERA", {"type": 'C', "value": 'PRESS'}, None),
("ADD_MARKER", {"type": 'M', "value": 'PRESS'}, None),
])
return keymap
def km_paint_stroke_modal(_params):
items = []
keymap = (
@ -9064,6 +9086,7 @@ def generate_keymaps(params=None):
km_view3d_move_modal(params),
km_view3d_zoom_modal(params),
km_view3d_dolly_modal(params),
km_view3d_location_scouting_capture_review_modal(params),
km_paint_stroke_modal(params),
km_sculpt_expand_modal(params),
km_sculpt_mesh_filter_modal_map(params),

View file

@ -30,7 +30,7 @@ namespace blender {
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 39
#define BLENDER_FILE_SUBVERSION 40
/* 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

View file

@ -20,6 +20,8 @@
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_windowmanager_types.h"
#include "DNA_xr_types.h"
#include "BLI_listbase_iterator.hh"
#include "BLI_string.h"
@ -805,6 +807,19 @@ void blo_do_versions_520(FileData * /*fd*/, Library * /*lib*/, Main *bmain)
}
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 502, 40)) {
for (wmWindowManager &wm : bmain->wm) {
wm.xr.session_settings.viewfinder_enabled = false;
wm.xr.session_settings.viewfinder_crosshair_enabled = true;
wm.xr.session_settings.viewfinder_hand = XR_VIEWFINDER_HAND_RIGHT;
wm.xr.session_settings.viewfinder_scale = 1.0f;
wm.xr.session_settings.viewfinder_passepartout_overscan = 0.5f;
wm.xr.session_settings.viewfinder_passepartout_opacity = 0.5f;
}
}
/**
* Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check.

View file

@ -66,6 +66,7 @@ void ED_view3d_draw_offscreen_simple(Depsgraph *depsgraph,
bool draw_background,
const char *viewname,
bool do_color_management,
Object *camera_override,
GPUOffScreen *ofs,
GPUViewport *viewport);

View file

@ -65,6 +65,7 @@ set(SRC
view3d_snap.cc
view3d_utils.cc
view3d_view.cc
view3d_vr_location_scouting.cc
view3d_intern.hh
view3d_navigate.hh
@ -105,6 +106,10 @@ endif()
if(WITH_XR_OPENXR)
add_definitions(-DWITH_XR_OPENXR)
list(APPEND INC
../../windowmanager/xr/
)
endif()
blender_add_lib(bf_editor_space_view3d "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")

View file

@ -1856,6 +1856,7 @@ void ED_view3d_draw_offscreen_simple(Depsgraph *depsgraph,
bool draw_background,
const char *viewname,
const bool do_color_management,
Object *camera_override,
GPUOffScreen *ofs,
GPUViewport *viewport)
{
@ -1927,7 +1928,14 @@ void ED_view3d_draw_offscreen_simple(Depsgraph *depsgraph,
v3d.object_type_exclude_viewport = object_type_exclude_viewport_override;
v3d.object_type_exclude_select = object_type_exclude_select_override;
rv3d.persp = RV3D_PERSP;
if (camera_override != nullptr) {
/* Override the active View3D camera and force camera perspective. */
rv3d.persp = RV3D_CAMOB;
v3d.camera = camera_override;
}
else {
rv3d.persp = RV3D_PERSP;
}
v3d.clip_start = clip_start;
v3d.clip_end = clip_end;
/* Actually not used since we pass in the projection matrix. */

View file

@ -194,6 +194,11 @@ void VIEW3D_OT_snap_cursor_to_active(wmOperatorType *ot);
void VIEW3D_OT_interactive_add(wmOperatorType *ot);
/* `view3d_location_scouting.cc` */
void vr_location_scouting_capture_review_modal_keymap(wmKeyConfig *keyconf);
void VIEW3D_OT_vr_location_scouting_capture_review(wmOperatorType *ot);
/* space_view3d.cc */
extern "C" const char *view3d_context_dir[]; /* doc access */

View file

@ -258,6 +258,10 @@ void view3d_operatortypes()
WM_operatortype_append(VIEW3D_OT_ruler_add);
WM_operatortype_append(VIEW3D_OT_ruler_remove);
#ifdef WITH_XR_OPENXR
WM_operatortype_append(VIEW3D_OT_vr_location_scouting_capture_review);
#endif
ed::transform::transform_operatortypes();
}
@ -275,6 +279,7 @@ void view3d_keymap(wmKeyConfig *keyconf)
viewzoom_modal_keymap(keyconf);
viewdolly_modal_keymap(keyconf);
viewplace_modal_keymap(keyconf);
vr_location_scouting_capture_review_modal_keymap(keyconf);
}
/** \} */

View file

@ -0,0 +1,376 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup spview3d
*/
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_object.hh"
#include "BKE_object_types.hh"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLT_translation.hh"
#include "DNA_camera_types.h"
#include "ED_screen.hh"
#include "ED_view3d.hh"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "WM_api.hh"
#ifdef WITH_XR_OPENXR
# include "wm_xr.hh"
#endif
namespace blender {
/* -------------------------------------------------------------------- */
/** \name Location Scouting Capture Review Operator
*
* \note This operator relies on the WM XR submodule, and is thus conditionally compiled
* if WITH_XR_OPENXR is defined. However, to ensure keymap files continuity, the
* operator modal keymap is always defined, and is conditionally assigned to the
* operator if WITH_XR_OPENXR is defined.
*
* \{ */
/* NOTE: these defines are saved in keymap files, do not change values but just add new ones */
enum {
CAPTURE_REVIEW_MODAL_EXIT = 1,
CAPTURE_REVIEW_MODAL_PREV,
CAPTURE_REVIEW_MODAL_NEXT,
CAPTURE_REVIEW_MODAL_ADD_CAMERA,
CAPTURE_REVIEW_MODAL_ADD_MARKER,
};
void vr_location_scouting_capture_review_modal_keymap(wmKeyConfig *keyconf)
{
static const EnumPropertyItem modal_items[] = {
{CAPTURE_REVIEW_MODAL_EXIT, "EXIT", 0, "Exit", ""},
{CAPTURE_REVIEW_MODAL_PREV, "PREVIOUS", 0, "Previous Capture", "Switch to previous capture"},
{CAPTURE_REVIEW_MODAL_NEXT, "NEXT", 0, "Next Capture", "Switch to next capture"},
{CAPTURE_REVIEW_MODAL_ADD_CAMERA,
"ADD_CAMERA",
0,
"Add Camera",
"Add Camera from current capture"},
{CAPTURE_REVIEW_MODAL_ADD_MARKER,
"ADD_MARKER",
0,
"Add Marker",
"Add Marker from current capture"},
{0, nullptr, 0, nullptr, nullptr},
};
wmKeyMap *keymap = WM_modalkeymap_find(keyconf,
"View3D VR Location Scouting Capture Review Modal");
/* This function is called for each space-type, only needs to add map once. */
if (keymap && keymap->modal_items) {
return;
}
keymap = WM_modalkeymap_ensure(
keyconf, "View3D VR Location Scouting Capture Review Modal", modal_items);
/* Assign map to operators. */
#ifdef WITH_XR_OPENXR
WM_modalkeymap_assign(keymap, "VIEW3D_OT_vr_location_scouting_capture_review");
#endif
}
#ifdef WITH_XR_OPENXR
struct CaptureReviewData {
/* Context. */
ScrArea *area;
RegionView3D *rv3d;
View3D *v3d;
/* Previous camera to restore on exit. */
Object *prev_view3d_cam_ob;
eRegionView3D_Persp prev_view3d_persp;
/* Fake camera object to set the View3D. */
Object *cam_ob;
Camera *cam_data;
};
static void location_scouting_review_draw_status(bContext *C, wmOperator *op)
{
WorkspaceStatus status(C);
status.opmodal(IFACE_("Exit"), op->type, CAPTURE_REVIEW_MODAL_EXIT);
status.opmodal("", op->type, CAPTURE_REVIEW_MODAL_PREV);
status.item(IFACE_("Previous"), ICON_NONE);
status.opmodal("", op->type, CAPTURE_REVIEW_MODAL_NEXT);
status.item(IFACE_("Next"), ICON_NONE);
status.opmodal("", op->type, CAPTURE_REVIEW_MODAL_ADD_CAMERA);
status.item(IFACE_("Add Camera"), ICON_NONE);
status.opmodal("", op->type, CAPTURE_REVIEW_MODAL_ADD_MARKER);
status.item(IFACE_("Add Marker"), ICON_NONE);
}
bool vr_location_scouting_capture_review_poll(bContext *C)
{
return !wm_xr_location_scouting_is_captures_empty(CTX_data_scene(C)) &&
ED_operator_region_view3d_active(C);
}
/* The capture review property is stored on the WM, registered by the VR Python add-on. */
static bool vr_location_scouting_capture_review_get_running_state(bContext *C)
{
PointerRNA wm_ptr = RNA_id_pointer_create(&CTX_wm_manager(C)->id);
PropertyRNA *state_prop = RNA_struct_find_property(&wm_ptr, "vr_capture_review_running");
/* Property wasn't found, VR add-on is probably not loaded. Shouldn't be possible. */
BLI_assert(state_prop != nullptr);
return RNA_property_boolean_get(&wm_ptr, state_prop);
}
static void vr_location_scouting_capture_review_set_running_state(bContext *C, const bool state)
{
PointerRNA wm_ptr = RNA_id_pointer_create(&CTX_wm_manager(C)->id);
PropertyRNA *state_prop = RNA_struct_find_property(&wm_ptr, "vr_capture_review_running");
/* Property wasn't found, VR add-on is probably not loaded. Shouldn't be possible. */
BLI_assert(state_prop != nullptr);
RNA_property_boolean_set(&wm_ptr, state_prop, state);
}
static wmOperatorStatus vr_location_scouting_capture_review_invoke(bContext *C,
wmOperator *op,
const wmEvent * /*event*/)
{
/* Operator invoked while already running, toggle off. */
if (vr_location_scouting_capture_review_get_running_state(C)) {
/* Set the running state (stored on the WM) to false, catched by the running operator modal. */
vr_location_scouting_capture_review_set_running_state(C, false);
return OPERATOR_CANCELLED;
}
View3D *v3d;
ARegion *region;
ED_view3d_context_user_region(C, &v3d, &region);
RegionView3D *rv3d = static_cast<RegionView3D *>(region->regiondata);
ScrArea *area = CTX_wm_area(C);
if (RV3D_LOCK_FLAGS(rv3d) & RV3D_LOCK_ANY_TRANSFORM) {
return OPERATOR_CANCELLED;
}
CaptureReviewData *op_data = MEM_new_zeroed<CaptureReviewData>("View3DReviewCaptureData");
op_data->area = area;
op_data->rv3d = rv3d;
op_data->v3d = v3d;
op_data->prev_view3d_cam_ob = v3d->camera;
ED_view3d_lastview_store(rv3d);
/* Store previous persp separately from #ED_view3d_lastview_store as setting rv3d->lpersp to
* CAMOB is unexpected by navigation logic. */
op_data->prev_view3d_persp = rv3d->persp;
/* Build a fake Camera object to set on the View3D. */
op_data->cam_ob = BKE_id_new_nomain<Object>("ReviewCaptureCamera");
op_data->cam_ob->type = OB_CAMERA;
/* Lock rotation to prevent user from exiting the review camera view. Re-using quadview flags. */
rv3d->viewlock |= RV3D_LOCK_ROTATION;
op_data->cam_data = BKE_id_new_nomain<Camera>("ReviewCaptureCameraData");
op_data->cam_ob->data = id_cast<ID *>(op_data->cam_data);
op->customdata = op_data;
/* Set running state. */
vr_location_scouting_capture_review_set_running_state(C, true);
WM_event_add_modal_handler(C, op);
location_scouting_review_draw_status(C, op);
return OPERATOR_RUNNING_MODAL;
}
static void vr_location_scouting_capture_review_exit(bContext *C, wmOperator *op)
{
CaptureReviewData *op_data = static_cast<CaptureReviewData *>(op->customdata);
RegionView3D *rv3d = op_data->rv3d;
/* Restore viewport, last view stored by #ED_view3d_lastview_store */
copy_qt_qt(rv3d->viewquat, rv3d->lviewquat);
rv3d->view = rv3d->lview;
rv3d->view_axis_roll = rv3d->lview_axis_roll;
rv3d->persp = op_data->prev_view3d_persp;
rv3d->viewlock &= ~RV3D_LOCK_ROTATION;
op_data->v3d->camera = op_data->prev_view3d_cam_ob;
vr_location_scouting_capture_review_set_running_state(C, false);
/* Redraw entire area (both the viewport and sidebar regions), clear status text. */
ED_area_tag_redraw(CTX_wm_area(C));
ED_workspace_status_text(C, nullptr);
/* Free data. */
BKE_id_free(nullptr, id_cast<ID *>(op_data->cam_ob));
BKE_id_free(nullptr, id_cast<ID *>(op_data->cam_data));
MEM_delete(op_data);
}
static void vr_location_scouting_capture_review_cancel(bContext *C, wmOperator *op)
{
vr_location_scouting_capture_review_exit(C, op);
}
static bool vr_location_scouting_capture_review_event(bContext *C, const wmEvent *event)
{
/* Return true if event is handled, false otherwise. */
if (event->type != EVT_MODAL_MAP) {
return false;
}
switch (event->val) {
case CAPTURE_REVIEW_MODAL_EXIT:
vr_location_scouting_capture_review_set_running_state(C, false);
break;
case CAPTURE_REVIEW_MODAL_PREV:
case CAPTURE_REVIEW_MODAL_NEXT: {
wmOperatorType *ot = WM_operatortype_find("VIEW3D_OT_vr_location_scouting_browse_captures",
false);
PointerRNA props_ptr = WM_operator_properties_create_ptr(ot);
RNA_boolean_set(&props_ptr, "backward", event->val == CAPTURE_REVIEW_MODAL_PREV);
WM_operator_name_call_ptr(C, ot, wm::OpCallContext::ExecDefault, &props_ptr, nullptr);
WM_operator_properties_free(&props_ptr);
break;
}
case CAPTURE_REVIEW_MODAL_ADD_CAMERA:
WM_operator_name_call(C,
"VIEW3D_OT_vr_location_scouting_add_camera_from_capture",
wm::OpCallContext::ExecDefault,
nullptr,
nullptr);
break;
case CAPTURE_REVIEW_MODAL_ADD_MARKER:
WM_operator_name_call(C,
"VIEW3D_OT_vr_location_scouting_add_marker_from_capture",
wm::OpCallContext::ExecDefault,
nullptr,
nullptr);
break;
}
return true;
}
static wmOperatorStatus vr_location_scouting_capture_review_modal(bContext *C,
wmOperator *op,
const wmEvent *event)
{
CaptureReviewData *op_data = static_cast<CaptureReviewData *>(op->customdata);
/* Exit review if the mouse leaves the viewport area, or if the active area editor changes.
* This prevents undefined behavior caused by the operator running without an active View3D
* area while still allowing the operator to be non-blocking for interactive sidebar UI. */
if ((ED_area_find_under_cursor(C, SPACE_TYPE_ANY, event->xy) != op_data->area) ||
(op_data->area->spacetype != SPACE_VIEW3D))
{
vr_location_scouting_capture_review_exit(C, op);
return OPERATOR_FINISHED;
}
/* Get the current capture. */
Scene *scene = CTX_data_scene(C);
auto capture = wm_xr_location_scouting_get_active_capture(scene);
if (!capture.has_value()) {
BKE_report(op->reports, RPT_INFO, "No VR captures to display, exiting capture review...");
vr_location_scouting_capture_review_exit(C, op);
return OPERATOR_FINISHED;
}
const bool event_handled = vr_location_scouting_capture_review_event(C, event);
/* Exit requested, state set by the operator invoke from UI, or event function from keymap. */
if (!vr_location_scouting_capture_review_get_running_state(C)) {
vr_location_scouting_capture_review_exit(C, op);
return OPERATOR_FINISHED;
}
/* Force perspective to camera. */
op_data->rv3d->persp = RV3D_CAMOB;
/* Set Camera data from capture. */
op_data->cam_data->lens = capture->lens_focal;
SET_FLAG_FROM_TEST(op_data->cam_data->dof.flag, capture->dof_enabled, CAM_DOF_ENABLED);
op_data->cam_data->dof.aperture_fstop = capture->dof_fstop;
op_data->cam_data->dof.focus_distance = capture->dof_distance;
/* Camera viewport display settings, set passepartout to emphasize that the modal is enabled. */
op_data->cam_data->flag |= CAM_SHOWPASSEPARTOUT;
op_data->cam_data->passepartalpha = 0.99f; /* *Almost* completely opaque. */
float capture_cam_mat[4][4];
quat_to_mat4(capture_cam_mat, capture->orientation_quat);
copy_v3_v3(capture_cam_mat[3], capture->position);
BKE_object_apply_mat4(op_data->cam_ob, capture_cam_mat, false, false);
/* Minimum eval without going through the depsgraph. */
BKE_object_to_mat4(op_data->cam_ob, op_data->cam_ob->runtime->object_to_world.ptr());
/* Set fake Camera object as the View3D camera. */
op_data->v3d->camera = op_data->cam_ob;
ED_area_tag_redraw(CTX_wm_area(C));
location_scouting_review_draw_status(C, op);
if (event_handled) {
return OPERATOR_RUNNING_MODAL;
}
return OPERATOR_PASS_THROUGH;
}
void VIEW3D_OT_vr_location_scouting_capture_review(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Location Scouting Capture Review";
ot->description = "Interactively review Location Scouting VR Captures";
ot->idname = "VIEW3D_OT_vr_location_scouting_capture_review";
/* Callbacks. */
ot->invoke = vr_location_scouting_capture_review_invoke;
ot->cancel = vr_location_scouting_capture_review_cancel;
ot->modal = vr_location_scouting_capture_review_modal;
ot->poll = vr_location_scouting_capture_review_poll;
}
#endif /* WITH_XR_OPENXR */
/** \} */
} // namespace blender

View file

@ -87,6 +87,33 @@ enum eXrPoseFlag : short {
};
ENUM_OPERATORS(eXrPoseFlag)
/* Location Scouting Viewfinder. */
enum eXrViewfinderHand { XR_VIEWFINDER_HAND_LEFT = 0, XR_VIEWFINDER_HAND_RIGHT = 1 };
enum eXrViewfinderMode {
XR_VIEWFINDER_MODE_LIVE = 0,
XR_VIEWFINDER_MODE_PLAYBACK = 1,
XR_VIEWFINDER_MODE_CONFIRM = 2
};
enum eXrViewfinderLiveAction {
XR_VIEWFINDER_ACTION_LIVE_LENS = 0,
XR_VIEWFINDER_ACTION_LIVE_DOF = 1,
XR_VIEWFINDER_ACTION_LIVE_FOCUS = 2,
XR_VIEWFINDER_ACTION_LIVE_APERTURE = 3
};
enum eXrViewfinderPlaybackAction {
XR_VIEWFINDER_ACTION_PB_BROWSE = 0,
XR_VIEWFINDER_ACTION_PB_PREVIEW = 1,
XR_VIEWFINDER_ACTION_PB_DELETE = 2
};
enum eXrViewfinderConfirmAction {
XR_VIEWFINDER_ACTION_CF_CONFIRM = 0,
XR_VIEWFINDER_ACTION_CF_CANCEL = 1
};
/**
* The following user and component path lengths are dependent on OpenXR's XR_MAX_PATH_LENGTH
* (256). A user path will be combined with a component path to identify an action binding, and
@ -115,7 +142,15 @@ struct XrSessionSettings {
char draw_flags = 0;
/** Draw style for controller visualization. */
char controller_draw_style = 0;
char _pad2[2] = {};
char viewfinder_enabled = 0;
char viewfinder_crosshair_enabled = 0;
char viewfinder_hand = 0; /* #eXrViewfinderHand */
char _pad2[3];
float viewfinder_scale = 1.0f;
float viewfinder_passepartout_overscan = 0.5f;
float viewfinder_passepartout_opacity = 0.5f;
/** Clipping distance. */
float clip_start = 0, clip_end = 0;

View file

@ -40,7 +40,7 @@ static wmXrData *rna_XrSession_wm_xr_data_get(PointerRNA *ptr)
/* Callers could also get XrSessionState pointer through ptr->data, but prefer if we just
* consistently pass wmXrData pointers to the WM_xr_xxx() API. */
BLI_assert(ELEM(ptr->type, RNA_XrSessionSettings, RNA_XrSessionState));
BLI_assert(ELEM(ptr->type, RNA_XrSessionSettings, RNA_XrSessionState, RNA_XrViewfinderState));
wmWindowManager *wm = (wmWindowManager *)ptr->owner_id;
BLI_assert(wm && (GS(wm->id.name) == ID_WM));
@ -1027,6 +1027,291 @@ static void rna_XrSessionState_viewer_pose_rotation_get(PointerRNA *ptr, float *
# endif
}
static PointerRNA rna_XrSessionState_viewfinder_get(PointerRNA *ptr)
{
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
wmXrViewfinderState *viewfinder = WM_xr_session_state_viewfinder_handle_get(xr);
return RNA_pointer_create_discrete(ptr->owner_id, RNA_XrViewfinderState, viewfinder);
# else
UNUSED_VARS(ptr);
return PointerRNA_NULL;
# endif
}
static void rna_XrSessionState_viewfinder_location_get(PointerRNA *ptr, float *r_values)
{
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_location_get(xr, r_values);
# else
UNUSED_VARS(ptr);
zero_v3(r_values);
# endif
}
static void rna_XrSessionState_viewfinder_orientation_get(PointerRNA *ptr, float *r_values)
{
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_orientation_get(xr, r_values);
# else
UNUSED_VARS(ptr);
unit_qt(r_values);
# endif
}
static void rna_XrSessionState_viewfinder_trigger_flash(PointerRNA ptr)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(&ptr);
WM_xr_session_state_viewfinder_trigger_flash(xr);
# else
UNUSED_VARS(ptr);
# endif
}
static void rna_XrSessionState_viewfinder_trigger_focus_indicator(PointerRNA ptr, bool hit_success)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(&ptr);
WM_xr_session_state_viewfinder_trigger_focus_indicator(xr, hit_success);
# else
UNUSED_VARS(ptr, hit_success);
# endif
}
static void rna_XrSessionState_viewfinder_reset_view_smoothing(PointerRNA ptr)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(&ptr);
WM_xr_session_state_viewfinder_reset_view_smoothing(xr);
# else
UNUSED_VARS(ptr);
# endif
}
static bool rna_XrSessionState_viewfinder_capture_dof_enabled_get(PointerRNA *ptr)
{
bool value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_capture_dof_enabled_get(xr, &value);
# else
UNUSED_VARS(ptr);
value = 1.0f;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_capture_dof_enabled_set(PointerRNA *ptr, bool value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_capture_dof_enabled_set(xr, value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static float rna_XrSessionState_viewfinder_capture_lens_focal_get(PointerRNA *ptr)
{
float value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_capture_lens_focal_get(xr, &value);
# else
UNUSED_VARS(ptr);
value = 1.0f;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_capture_lens_focal_set(PointerRNA *ptr, float value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_capture_lens_focal_set(xr, value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static float rna_XrSessionState_viewfinder_capture_dof_fstop_get(PointerRNA *ptr)
{
float value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_capture_dof_fstop_get(xr, &value);
# else
UNUSED_VARS(ptr);
value = 1.0f;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_capture_dof_fstop_set(PointerRNA *ptr, float value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_capture_dof_fstop_set(xr, value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static float rna_XrSessionState_viewfinder_capture_dof_distance_get(PointerRNA *ptr)
{
float value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_capture_dof_distance_get(xr, &value);
# else
UNUSED_VARS(ptr);
value = 1.0f;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_capture_dof_distance_set(PointerRNA *ptr, float value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_capture_dof_distance_set(xr, value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static bool rna_XrSessionState_viewfinder_playback_show_active_capture_in_space_enabled_get(
PointerRNA *ptr)
{
bool value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_playback_show_active_capture_in_space_enabled_get(xr, &value);
# else
UNUSED_VARS(ptr);
value = 1.0f;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_playback_show_active_capture_in_space_enabled_set(
PointerRNA *ptr, bool value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
WM_xr_session_state_viewfinder_playback_show_active_capture_in_space_enabled_set(xr, value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static int rna_XrSessionState_viewfinder_active_mode_get(PointerRNA *ptr)
{
int value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
eXrViewfinderMode enum_value;
WM_xr_session_state_viewfinder_active_mode_get(xr, &enum_value);
value = static_cast<int>(enum_value);
# else
UNUSED_VARS(ptr);
value = 0;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_active_mode_set(PointerRNA *ptr, int value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
const eXrViewfinderMode enum_value = static_cast<eXrViewfinderMode>(value);
WM_xr_session_state_viewfinder_active_mode_set(xr, enum_value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static int rna_XrSessionState_viewfinder_active_action_live_get(PointerRNA *ptr)
{
int value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
eXrViewfinderLiveAction enum_value;
WM_xr_session_state_viewfinder_active_action_live_get(xr, &enum_value);
value = static_cast<int>(enum_value);
# else
UNUSED_VARS(ptr);
value = 0;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_active_action_live_set(PointerRNA *ptr, int value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
const eXrViewfinderLiveAction enum_value = static_cast<eXrViewfinderLiveAction>(value);
WM_xr_session_state_viewfinder_active_action_live_set(xr, enum_value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static int rna_XrSessionState_viewfinder_active_action_playback_get(PointerRNA *ptr)
{
int value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
eXrViewfinderPlaybackAction enum_value;
WM_xr_session_state_viewfinder_active_action_playback_get(xr, &enum_value);
value = static_cast<int>(enum_value);
# else
UNUSED_VARS(ptr);
value = 0;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_active_action_playback_set(PointerRNA *ptr, int value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
const eXrViewfinderPlaybackAction enum_value = static_cast<eXrViewfinderPlaybackAction>(value);
WM_xr_session_state_viewfinder_active_action_playback_set(xr, enum_value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static int rna_XrSessionState_viewfinder_active_action_confirm_get(PointerRNA *ptr)
{
int value;
# ifdef WITH_XR_OPENXR
const wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
eXrViewfinderConfirmAction enum_value;
WM_xr_session_state_viewfinder_active_action_confirm_get(xr, &enum_value);
value = static_cast<int>(enum_value);
# else
UNUSED_VARS(ptr);
value = 0;
# endif
return value;
}
static void rna_XrSessionState_viewfinder_active_action_confirm_set(PointerRNA *ptr, int value)
{
# ifdef WITH_XR_OPENXR
wmXrData *xr = rna_XrSession_wm_xr_data_get(ptr);
const eXrViewfinderConfirmAction enum_value = static_cast<eXrViewfinderConfirmAction>(value);
WM_xr_session_state_viewfinder_active_action_confirm_set(xr, enum_value);
# else
UNUSED_VARS(ptr, value);
# endif
}
static void rna_XrSessionState_nav_location_get(PointerRNA *ptr, float *r_values)
{
# ifdef WITH_XR_OPENXR
@ -1965,6 +2250,20 @@ static void rna_def_xr_session_settings(BlenderRNA *brna)
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem viewfinder_hands[] = {
{XR_VIEWFINDER_HAND_LEFT,
"LEFT",
0,
"Left",
"Place the viewfinder on the left hand controller"},
{XR_VIEWFINDER_HAND_RIGHT,
"RIGHT",
0,
"Right",
"Place the viewfinder on the right hand controller"},
{0, nullptr, 0, nullptr, nullptr},
};
srna = RNA_def_struct(brna, "XrSessionSettings", nullptr);
RNA_def_struct_ui_text(srna, "XR Session Settings", "");
@ -2057,6 +2356,48 @@ static void rna_def_xr_session_settings(BlenderRNA *brna)
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_COLOR);
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "viewfinder_enabled", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_boolean_default(prop, true);
RNA_def_property_ui_text(prop, "Viewfinder Enabled", "Enable the Location Scouting Viewfinder");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "viewfinder_hand", PROP_ENUM, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_enum_default(prop, XR_VIEWFINDER_HAND_LEFT);
RNA_def_property_enum_items(prop, viewfinder_hands);
RNA_def_property_ui_text(
prop, "Viewfinder Hand", "Hand on which to place the Location Scouting Viewfinder");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "viewfinder_scale", PROP_FLOAT, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_range(prop, -3.0f, FLT_MAX);
RNA_def_property_ui_range(prop, 0.001f, 3.0f, 0.01 * 100, 3);
RNA_def_property_ui_text(prop, "Viewfinder Scale", "Location Scouting Viewfinder size scale");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "viewfinder_crosshair_enabled", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_boolean_default(prop, true);
RNA_def_property_ui_text(
prop, "Viewfinder Crosshair Enabled", "Enable the Viewfinder Crosshair");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "viewfinder_passepartout_overscan", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_ui_text(prop,
"Viewfinder Passepartout Overscan",
"Border size of the Viewfinder Passepartout overlay");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "viewfinder_passepartout_opacity", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_ui_text(prop,
"Viewfinder Passepartout Opacity",
"Opacity of the darkened Viewfinder Passepartout overlay");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
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, 0.1 * 100, 3);
@ -2130,6 +2471,12 @@ static void rna_def_xr_session_state(BlenderRNA *brna)
srna = RNA_def_struct(brna, "XrSessionState", nullptr);
RNA_def_struct_ui_text(srna, "Session State", "Runtime state information about the VR session");
/* XrViewfinderState */
prop = RNA_def_pointer(
srna, "viewfinder", "XrViewfinderState", "Viewfinder", "Viewfinder State");
RNA_def_property_pointer_funcs(
prop, "rna_XrSessionState_viewfinder_get", nullptr, nullptr, nullptr);
func = RNA_def_function(srna, "is_running", "rna_XrSessionState_is_running");
RNA_def_function_ui_description(func, "Query if the VR session is currently running");
RNA_def_function_flag(func, FUNC_NO_SELF);
@ -2486,6 +2833,201 @@ static void rna_def_xr_session_state(BlenderRNA *brna)
/** \} */
/* -------------------------------------------------------------------- */
/** \name XR Session State Viewfinder
* \{ */
static void rna_def_xr_session_state_viewfinder(BlenderRNA *brna)
{
StructRNA *srna;
FunctionRNA *func;
PropertyRNA *parm, *prop;
static const EnumPropertyItem viewfinder_modes[] = {
{XR_VIEWFINDER_MODE_LIVE,
"LIVE",
ICON_RESTRICT_RENDER_OFF,
"Live Mode",
"Capture a shot using the viewfinder"},
{XR_VIEWFINDER_MODE_PLAYBACK,
"PLAYBACK",
ICON_IMAGE_DATA,
"Playback Mode",
"Preview and playback captured shots in the viewfinder"},
{XR_VIEWFINDER_MODE_CONFIRM,
"CONFIRM",
ICON_CHECKMARK,
"Confirmation Mode",
"Confirm user action"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem viewfinder_live_actions[] = {
{XR_VIEWFINDER_ACTION_LIVE_LENS, "LENS", ICON_VIEW_ZOOM, "Focal Length Control", nullptr},
{XR_VIEWFINDER_ACTION_LIVE_DOF, "DOF", ICON_COMMUNITY, "Toggle Depth of Field", nullptr},
{XR_VIEWFINDER_ACTION_LIVE_FOCUS, "FOCUS", ICON_PIVOT_BOUNDBOX, "Set Focus Point", nullptr},
{XR_VIEWFINDER_ACTION_LIVE_APERTURE, "APERTURE", ICON_PROP_CON, "Aperture Control", nullptr},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem viewfinder_playback_actions[] = {
{XR_VIEWFINDER_ACTION_PB_BROWSE,
"BROWSE",
ICON_RENDERLAYERS,
"Browse captured shots",
nullptr},
{XR_VIEWFINDER_ACTION_PB_PREVIEW,
"PREVIEW",
ICON_OUTLINER_OB_CAMERA,
"Preview selected shot in space"},
{XR_VIEWFINDER_ACTION_PB_DELETE, "DELETE", ICON_TRASH, "Delete selected shot"},
{0, nullptr, 0, nullptr, nullptr}};
static const EnumPropertyItem viewfinder_confirm_actions[] = {
{XR_VIEWFINDER_ACTION_CF_CANCEL, "CANCEL", ICON_X, "Cancel", nullptr},
{XR_VIEWFINDER_ACTION_CF_CONFIRM, "CONFIRM", ICON_CHECKMARK, "Confirm", nullptr},
{0, nullptr, 0, nullptr, nullptr}};
srna = RNA_def_struct(brna, "XrViewfinderState", nullptr);
RNA_def_struct_ui_text(srna,
"Viewfinder State",
"Runtime state information about the VR Location Scouting Viewfinder");
prop = RNA_def_property(srna, "location", PROP_FLOAT, PROP_TRANSLATION);
RNA_def_property_array(prop, 3);
RNA_def_property_float_funcs(
prop, "rna_XrSessionState_viewfinder_location_get", nullptr, nullptr);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(
prop, "Viewfinder Location", "Last known location of the viewfinder in world space");
prop = RNA_def_property(srna, "orientation", PROP_FLOAT, PROP_QUATERNION);
RNA_def_property_array(prop, 4);
RNA_def_property_float_funcs(
prop, "rna_XrSessionState_viewfinder_orientation_get", nullptr, nullptr);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(
prop, "Viewfinder Rotation", "Last known orientation of the viewfinder in world space");
func = RNA_def_function(srna, "trigger_flash", "rna_XrSessionState_viewfinder_trigger_flash");
RNA_def_function_ui_description(func,
"Trigger the Viewfinder flash to indicate a shot was captured");
RNA_def_function_flag(func, FUNC_SELF_AS_RNA);
func = RNA_def_function(
srna, "trigger_focus_indicator", "rna_XrSessionState_viewfinder_trigger_focus_indicator");
RNA_def_function_ui_description(
func, "Blink the Viewfinder crosshair to indicate whether a focus action hit a target");
RNA_def_function_flag(func, FUNC_SELF_AS_RNA);
parm = RNA_def_boolean(func,
"hit_success",
false,
"Hit success",
"True to blink the success color, False to blink the miss color");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
func = RNA_def_function(
srna, "reset_view_smoothing", "rna_XrSessionState_viewfinder_reset_view_smoothing");
RNA_def_function_ui_description(func, "Reset the Viewfinder continuous view smoothing");
RNA_def_function_flag(func, FUNC_SELF_AS_RNA);
prop = RNA_def_property(srna, "capture_dof_enabled", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_boolean_funcs(prop,
"rna_XrSessionState_viewfinder_capture_dof_enabled_get",
"rna_XrSessionState_viewfinder_capture_dof_enabled_set");
RNA_def_property_ui_text(prop,
"Viewfinder Capture Depth of Field Enabled",
"Enable viewfinder capture depth of field");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "capture_lens_focal", PROP_FLOAT, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_float_funcs(prop,
"rna_XrSessionState_viewfinder_capture_lens_focal_get",
"rna_XrSessionState_viewfinder_capture_lens_focal_set",
nullptr);
RNA_def_property_ui_text(prop,
"Viewfinder Capture Focal Length",
"Viewfinder capture focal length value in millimeters");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "capture_dof_distance", PROP_FLOAT, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_float_funcs(prop,
"rna_XrSessionState_viewfinder_capture_dof_distance_get",
"rna_XrSessionState_viewfinder_capture_dof_distance_set",
nullptr);
RNA_def_property_ui_text(prop,
"Viewfinder Capture Focus Distance",
"Viewfinder capture distance to the focus point for depth of field");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "capture_dof_fstop", PROP_FLOAT, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_float_funcs(prop,
"rna_XrSessionState_viewfinder_capture_dof_fstop_get",
"rna_XrSessionState_viewfinder_capture_dof_fstop_set",
nullptr);
RNA_def_property_ui_text(prop, "Viewfinder Capture F-Stop", "Viewfinder capture f-stop ratio");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(
srna, "playback_show_active_capture_in_space_enabled", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_boolean_funcs(
prop,
"rna_XrSessionState_viewfinder_playback_show_active_capture_in_space_enabled_get",
"rna_XrSessionState_viewfinder_playback_show_active_capture_in_space_enabled_set");
RNA_def_property_ui_text(prop,
"Viewfinder Playback Show active capture in space enabled",
"Display active capture in space when in Viewfinder Playback mode");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "active_mode", PROP_ENUM, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_enum_funcs(prop,
"rna_XrSessionState_viewfinder_active_mode_get",
"rna_XrSessionState_viewfinder_active_mode_set",
nullptr);
RNA_def_property_enum_items(prop, viewfinder_modes);
RNA_def_property_ui_text(prop, "Viewfinder Mode", "Active viewfinder mode, live or playback");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "active_action_live", PROP_ENUM, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_enum_funcs(prop,
"rna_XrSessionState_viewfinder_active_action_live_get",
"rna_XrSessionState_viewfinder_active_action_live_set",
nullptr);
RNA_def_property_enum_items(prop, viewfinder_live_actions);
RNA_def_property_ui_text(prop, "Viewfinder Live Action", "Active viewfinder live action");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "active_action_playback", PROP_ENUM, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_enum_funcs(prop,
"rna_XrSessionState_viewfinder_active_action_playback_get",
"rna_XrSessionState_viewfinder_active_action_playback_set",
nullptr);
RNA_def_property_enum_items(prop, viewfinder_playback_actions);
RNA_def_property_ui_text(
prop, "Viewfinder Playback Action", "Active viewfinder playback action");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
prop = RNA_def_property(srna, "active_action_confirm", PROP_ENUM, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_enum_funcs(prop,
"rna_XrSessionState_viewfinder_active_action_confirm_get",
"rna_XrSessionState_viewfinder_active_action_confirm_set",
nullptr);
RNA_def_property_enum_items(prop, viewfinder_confirm_actions);
RNA_def_property_ui_text(prop, "Viewfinder Confirm Action", "Active viewfinder confirm action");
RNA_def_property_update(prop, NC_WM | ND_XR_DATA_CHANGED, nullptr);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name XR Event Data
* \{ */
@ -2598,6 +3140,7 @@ void RNA_def_xr(BlenderRNA *brna)
rna_def_xr_actionmap(brna);
rna_def_xr_session_settings(brna);
rna_def_xr_session_state(brna);
rna_def_xr_session_state_viewfinder(brna);
rna_def_xr_eventdata(brna);
RNA_define_animate_sdna(true);

View file

@ -8,6 +8,7 @@ set(INC
gizmo/intern
../compositor
../editors/include
../editors/interface
../gpu
../makesrna
../../../intern/memutil
@ -196,6 +197,7 @@ if(WITH_XR_OPENXR)
xr/intern/wm_xr_action.cc
xr/intern/wm_xr_actionmap.cc
xr/intern/wm_xr_draw.cc
xr/intern/wm_xr_location_scouting.cc
xr/intern/wm_xr_operators.cc
xr/intern/wm_xr_session.cc

View file

@ -78,6 +78,7 @@ struct wmNDOFMotionData;
#ifdef WITH_XR_OPENXR
struct wmXrRuntimeData;
struct wmXrSessionState;
struct wmXrViewfinderState;
#endif
namespace bke::id {
@ -2277,17 +2278,27 @@ bool WM_xr_session_exists(const wmXrData *xr);
* Check if the session is running, according to the OpenXR definition.
*/
bool WM_xr_session_is_ready(const wmXrData *xr);
wmXrSessionState *WM_xr_session_state_handle_get(const wmXrData *xr);
wmXrViewfinderState *WM_xr_session_state_viewfinder_handle_get(const wmXrData *xr);
bContext *WM_xr_session_context_get(const wmXrData *xr);
bContext *WM_xr_session_context_ensure(wmXrData *xr, const wmWindowManager *wm);
void WM_xr_session_base_pose_reset(wmXrData *xr);
void WM_xr_session_state_navigation_reset(wmXrSessionState *state);
void WM_xr_session_state_viewfinder_reset(wmXrSessionState *state);
void WM_xr_session_state_vignette_activate(wmXrData *xr);
void WM_xr_session_state_vignette_update(wmXrSessionState *state);
bool WM_xr_session_state_viewer_pose_location_get(const wmXrData *xr, float r_location[3]);
bool WM_xr_session_state_viewer_pose_rotation_get(const wmXrData *xr, float r_rotation[4]);
bool WM_xr_session_state_viewer_pose_matrix_info_get(const wmXrData *xr,
float r_viewmat[4][4],
float *r_focal_len);
bool WM_xr_session_state_viewer_scale_get(const wmXrData *xr, float *r_scale);
bool WM_xr_session_state_controller_grip_location_get(const wmXrData *xr,
unsigned int subaction_idx,
float r_location[3]);
@ -2300,16 +2311,52 @@ bool WM_xr_session_state_controller_aim_location_get(const wmXrData *xr,
bool WM_xr_session_state_controller_aim_rotation_get(const wmXrData *xr,
unsigned int subaction_idx,
float r_rotation[4]);
bool WM_xr_session_state_nav_location_get(const wmXrData *xr, float r_location[3]);
void WM_xr_session_state_nav_location_set(wmXrData *xr, const float location[3]);
bool WM_xr_session_state_nav_rotation_get(const wmXrData *xr, float r_rotation[4]);
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);
bool WM_xr_session_state_viewer_scale_get(const wmXrData *xr, float *r_scale);
void WM_xr_session_state_navigation_reset(wmXrSessionState *state);
void WM_xr_session_state_vignette_activate(wmXrData *xr);
void WM_xr_session_state_vignette_update(wmXrSessionState *state);
bool WM_xr_session_state_viewfinder_location_get(const wmXrData *xr, float r_location[3]);
bool WM_xr_session_state_viewfinder_orientation_get(const wmXrData *xr, float r_rotation[4]);
void WM_xr_session_state_viewfinder_trigger_flash(wmXrData *xr);
void WM_xr_session_state_viewfinder_trigger_focus_indicator(wmXrData *xr, bool hit_success);
void WM_xr_session_state_viewfinder_reset_view_smoothing(wmXrData *xr);
bool WM_xr_session_state_viewfinder_capture_dof_enabled_get(const wmXrData *xr,
bool *r_dof_enabled);
void WM_xr_session_state_viewfinder_capture_dof_enabled_set(wmXrData *xr, bool dof_enabled);
bool WM_xr_session_state_viewfinder_capture_lens_focal_get(const wmXrData *xr,
float *r_lens_focal);
void WM_xr_session_state_viewfinder_capture_lens_focal_set(wmXrData *xr, float lens_focal);
bool WM_xr_session_state_viewfinder_capture_dof_distance_get(const wmXrData *xr,
float *r_dof_distance);
void WM_xr_session_state_viewfinder_capture_dof_distance_set(wmXrData *xr, float dof_distance);
bool WM_xr_session_state_viewfinder_capture_dof_fstop_get(const wmXrData *xr, float *r_dof_fstop);
void WM_xr_session_state_viewfinder_capture_dof_fstop_set(wmXrData *xr, float dof_fstop);
bool WM_xr_session_state_viewfinder_playback_show_active_capture_in_space_enabled_get(
const wmXrData *xr, bool *r_enabled);
void WM_xr_session_state_viewfinder_playback_show_active_capture_in_space_enabled_set(
wmXrData *xr, bool enabled);
bool WM_xr_session_state_viewfinder_active_mode_get(const wmXrData *xr, eXrViewfinderMode *r_mode);
void WM_xr_session_state_viewfinder_active_mode_set(wmXrData *xr, eXrViewfinderMode mode);
bool WM_xr_session_state_viewfinder_active_action_live_get(const wmXrData *xr,
eXrViewfinderLiveAction *r_action);
void WM_xr_session_state_viewfinder_active_action_live_set(wmXrData *xr,
eXrViewfinderLiveAction action);
bool WM_xr_session_state_viewfinder_active_action_playback_get(
const wmXrData *xr, eXrViewfinderPlaybackAction *r_action);
void WM_xr_session_state_viewfinder_active_action_playback_set(wmXrData *xr,
eXrViewfinderPlaybackAction action);
bool WM_xr_session_state_viewfinder_active_action_confirm_get(
const wmXrData *xr, eXrViewfinderConfirmAction *r_action);
void WM_xr_session_state_viewfinder_active_action_confirm_set(wmXrData *xr,
eXrViewfinderConfirmAction action);
ARegionType *WM_xr_surface_controller_region_type_get();

View file

@ -103,7 +103,7 @@ static void wm_xr_draw_matrices_create(const wmXrDrawData *draw_data,
wm_xr_pose_to_imat(&eye_pose, eye_inv);
/* Apply base pose and navigation. */
wm_xr_pose_scale_to_imat(&draw_data->base_pose, draw_data->base_scale, base_inv);
wm_xr_pose_scale_to_imat(&session_state->base_pose, session_state->base_scale, base_inv);
wm_xr_pose_scale_to_imat(&session_state->nav_pose_last_actions_sync,
session_state->viewer_scale_last_actions_sync,
nav_inv);
@ -183,6 +183,11 @@ void wm_xr_draw_view(const GHOST_XrDrawViewInfo *draw_view, void *customdata)
* #do_depsgraph callback. Thus, obtain the depsgraph directly without evaluating it. */
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(xr_context);
if (draw_view->view_idx == 0) {
/* Only render location scouting viewfinder on first eye draw. */
wm_xr_viewfinder_render_view(xr_data);
}
/* Draws the view into the surface_data->viewport's frame-buffers. */
ED_view3d_draw_offscreen_simple(depsgraph,
scene,
@ -204,6 +209,7 @@ void wm_xr_draw_view(const GHOST_XrDrawViewInfo *draw_view, void *customdata)
true,
nullptr,
false,
nullptr,
vp->offscreen,
vp->viewport);
@ -432,7 +438,7 @@ static void wm_xr_controller_aim_draw(const XrSessionSettings *settings, wmXrSes
immUnbindProgram();
}
void wm_xr_draw_controllers(const bContext * /*C*/, ARegion * /*region*/, void *customdata)
void wm_xr_draw_controllers(const bContext *C, ARegion * /*region*/, void *customdata)
{
wmXrData *xr = static_cast<wmXrData *>(customdata);
const XrSessionSettings *settings = &xr->session_settings;
@ -441,6 +447,7 @@ void wm_xr_draw_controllers(const bContext * /*C*/, ARegion * /*region*/, void *
wm_xr_controller_model_draw(settings, xr_context, state);
wm_xr_controller_aim_draw(settings, state);
wm_xr_viewfinder_draw(C, settings, state);
}
} // namespace blender

View file

@ -23,6 +23,8 @@ namespace blender {
struct bContext;
struct ARegion;
struct Camera;
struct GPUOffScreen;
struct Object;
struct wmWindow;
struct wmWindowManager;
@ -30,6 +32,42 @@ struct wmXrActionSet;
struct wmXrController;
struct wmXrData;
namespace gpu {
class Texture;
}
struct wmXrViewfinderState {
/* Internal Runtime values. */
float capture_position[3];
float capture_orientation_quat[4];
GPUOffScreen *framebuffer;
gpu::Texture *backside_logo_texture;
Camera *render_cam_data_id;
double smoothing_delta_t;
/* Runtime values set by RNA methods called from Python. */
double last_flash_trigger_time;
double last_focus_hit_time;
bool last_focus_hit_success;
/* Capture settings. */
bool capture_dof_enabled;
float capture_lens_focal;
float capture_dof_distance;
float capture_dof_fstop;
/* Playback settings. */
bool playback_show_active_capture_in_space_enabled;
/** Active modes, concept differs from the rest of the Blender UI. */
eXrViewfinderMode active_mode;
eXrViewfinderLiveAction active_action_live;
eXrViewfinderPlaybackAction active_action_playback;
eXrViewfinderConfirmAction active_action_confirm;
};
struct wmXrSessionState {
bool is_started;
@ -41,6 +79,15 @@ struct wmXrSessionState {
float viewer_mat_base[4][4];
float focal_len;
wmXrViewfinderState viewfinder;
/** The pose (location + rotation) to which eye deltas will be applied to when drawing (world
* space). With positional tracking enabled, it should be the same as the base pose, when
* disabled it also contains a location delta from the moment the option was toggled. */
GHOST_XrPose base_pose;
/** Base scale (uniform, world space). */
float base_scale;
/** Copy of XrSessionSettings.base_pose_ data to detect changes that need
* resetting to base pose. */
char prev_base_pose_type; /* #eXRSessionBasePoseType. */
@ -128,12 +175,6 @@ struct wmXrDrawData {
wmXrData *xr_data;
wmXrSurfaceData *surface_data;
/** The pose (location + rotation) to which eye deltas will be applied to when drawing (world
* space). With positional tracking enabled, it should be the same as the base pose, when
* disabled it also contains a location delta from the moment the option was toggled. */
GHOST_XrPose base_pose;
/** Base scale (uniform, world space). */
float base_scale;
/** Offset to _subtract_ from the OpenXR eye and viewer pose to get the wanted effective pose
* (e.g. a pose exactly at the landmark position). */
float eye_position_ofs[3]; /* Local/view space. */
@ -277,4 +318,12 @@ bool wm_xr_passthrough_enabled(void *customdata);
*/
void wm_xr_disable_passthrough(void *customdata);
/* `wm_xr_location_scouting.cc` */
bool wm_xr_viewfinder_operator_event_match_hand(bContext *C, const wmEvent *event);
void wm_xr_viewfinder_render_view(wmXrData *xr_data);
void wm_xr_viewfinder_draw(const bContext *C,
const XrSessionSettings *settings,
wmXrSessionState *state);
} // namespace blender

File diff suppressed because it is too large Load diff

View file

@ -420,6 +420,10 @@ static wmOperatorStatus wm_xr_navigation_grab_invoke(bContext *C,
return OPERATOR_PASS_THROUGH;
}
if (wm_xr_viewfinder_operator_event_match_hand(C, event)) {
return OPERATOR_PASS_THROUGH;
}
const wmXrActionData *actiondata = static_cast<const wmXrActionData *>(event->customdata);
wm_xr_grab_init(op);
@ -787,8 +791,25 @@ static wmOperatorStatus wm_xr_navigation_fly_invoke(bContext *C,
}
wmWindowManager *wm = CTX_wm_manager(C);
wmXrData *xr = &wm->xr;
wm_xr_fly_init(op, &wm->xr);
if (wm_xr_viewfinder_operator_event_match_hand(C, event)) {
const bool swap_hands = xr->runtime->session_state.swap_hands;
const eXrFlyMode mode = eXrFlyMode(RNA_enum_get(op->ptr, swap_hands ? "alt_mode" : "mode"));
const eXrViewfinderHand viewfinder_hand = eXrViewfinderHand(
xr->session_settings.viewfinder_hand);
const bool clashes_with_viewfinder = (ELEM(mode, XR_FLY_VIEWER_LEFT, XR_FLY_VIEWER_RIGHT) &&
viewfinder_hand == XR_VIEWFINDER_HAND_LEFT) ||
(ELEM(mode, XR_FLY_TURNLEFT, XR_FLY_TURNRIGHT) &&
viewfinder_hand == XR_VIEWFINDER_HAND_RIGHT);
if (clashes_with_viewfinder) {
return OPERATOR_PASS_THROUGH;
}
}
wm_xr_fly_init(op, xr);
WM_event_add_modal_handler(C, op);
@ -1553,6 +1574,10 @@ static wmOperatorStatus wm_xr_navigation_teleport_invoke(bContext *C,
return OPERATOR_PASS_THROUGH;
}
if (wm_xr_viewfinder_operator_event_match_hand(C, event)) {
return OPERATOR_PASS_THROUGH;
}
wm_xr_navigation_teleport_init(op);
const wmOperatorStatus retval = op->type->modal(C, op, event);
@ -1799,6 +1824,17 @@ static wmOperatorStatus wm_xr_navigation_reset_exec(bContext *C, wmOperator *op)
return OPERATOR_FINISHED;
}
static wmOperatorStatus wm_xr_navigation_reset_invoke(bContext *C,
wmOperator *op,
const wmEvent *event)
{
if (wm_xr_viewfinder_operator_event_match_hand(C, event)) {
return OPERATOR_CANCELLED;
}
return wm_xr_navigation_reset_exec(C, op);
}
static void WM_OT_xr_navigation_reset(wmOperatorType *ot)
{
/* Identifiers. */
@ -1807,6 +1843,7 @@ static void WM_OT_xr_navigation_reset(wmOperatorType *ot)
ot->description = "Reset VR navigation deltas relative to session base pose";
/* Callbacks. */
ot->invoke = wm_xr_navigation_reset_invoke;
ot->exec = wm_xr_navigation_reset_exec;
ot->poll = wm_xr_operator_sessionactive;

View file

@ -10,7 +10,9 @@
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_idprop.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "BKE_scene.hh"
#include "BKE_screen.hh"
@ -37,6 +39,11 @@
#include "GPU_batch.hh"
#include "GPU_viewport.hh"
#include "IMB_imbuf.hh"
#include "UI_interface_icons.hh"
#include "UI_resources.hh"
#include "MEM_guardedalloc.h"
#include "WM_api.hh"
@ -54,6 +61,11 @@ static CLG_LogRef LOG = {"wm.xr"};
/* -------------------------------------------------------------------- */
static void wm_xr_session_base_pose_calc(const Scene *scene,
const XrSessionSettings *settings,
GHOST_XrPose *r_base_pose,
float *r_base_scale);
static void wm_xr_session_create_cb()
{
Main *bmain = G_MAIN;
@ -67,8 +79,13 @@ static void wm_xr_session_create_cb()
wm_xr_session_actions_init(xr_data);
const bContext *xr_context = WM_xr_session_context_ensure(xr_data, wm);
const Scene *scene = CTX_data_scene(xr_context);
wm_xr_session_base_pose_calc(scene, settings, &state->base_pose, &state->base_scale);
/* Initialize navigation. */
WM_xr_session_state_navigation_reset(state);
WM_xr_session_state_viewfinder_reset(state);
if (settings->base_scale < FLT_EPSILON) {
settings->base_scale = 1.0f;
}
@ -93,6 +110,8 @@ static void wm_xr_session_controller_data_free(wmXrSessionState *state)
void wm_xr_session_data_free(wmXrSessionState *state)
{
wm_xr_session_controller_data_free(state);
BKE_id_free(nullptr, id_cast<ID *>(state->viewfinder.render_cam_data_id));
GPU_TEXTURE_FREE_SAFE(state->viewfinder.backside_logo_texture);
}
static void wm_xr_session_exit_cb(void *customdata)
@ -201,14 +220,9 @@ static void wm_xr_session_base_pose_calc(const Scene *scene,
static void wm_xr_session_draw_data_populate(wmXrData *xr_data, wmXrDrawData *r_draw_data)
{
const XrSessionSettings *settings = &xr_data->session_settings;
memset(r_draw_data, 0, sizeof(*r_draw_data));
r_draw_data->xr_data = xr_data;
r_draw_data->surface_data = static_cast<wmXrSurfaceData *>(g_xr_surface->customdata);
Scene *scene = CTX_data_scene(WM_xr_session_context_get(xr_data));
wm_xr_session_base_pose_calc(scene, settings, &r_draw_data->base_pose, &r_draw_data->base_scale);
}
wmWindow *wm_xr_session_root_window_or_fallback_get(const wmWindowManager *wm,
@ -292,6 +306,7 @@ void wm_xr_session_draw_data_update(wmXrSessionState *state,
}
/* Reset navigation. */
WM_xr_session_state_navigation_reset(state);
WM_xr_session_state_viewfinder_reset(state);
break;
case SESSION_STATE_EVENT_POSITION_TRACKING_TOGGLE:
if (use_position_tracking) {
@ -384,7 +399,7 @@ void wm_xr_session_state_update(const XrSessionSettings *settings,
wm_xr_pose_to_mat(&viewer_pose, viewer_mat);
/* Apply base pose and navigation. */
wm_xr_pose_scale_to_mat(&draw_data->base_pose, draw_data->base_scale, base_mat);
wm_xr_pose_scale_to_mat(&state->base_pose, state->base_scale, base_mat);
wm_xr_pose_scale_to_mat(&state->nav_pose_last_actions_sync, state->viewer_scale, nav_mat);
mul_m4_m4m4(state->viewer_mat_base, base_mat, viewer_mat);
mul_m4_m4m4(viewer_mat, nav_mat, state->viewer_mat_base);
@ -392,7 +407,7 @@ void wm_xr_session_state_update(const XrSessionSettings *settings,
/* Save final viewer pose and viewmat. */
mat4_to_loc_quat(state->viewer_pose.position, state->viewer_pose.orientation_quat, viewer_mat);
wm_xr_pose_scale_to_imat(&state->viewer_pose,
draw_data->base_scale * state->viewer_scale_last_actions_sync,
state->base_scale * state->viewer_scale_last_actions_sync,
state->viewer_viewmat);
/* No idea why, but multiplying by two seems to make it match the VR view more. */
@ -401,8 +416,8 @@ void wm_xr_session_state_update(const XrSessionSettings *settings,
DEFAULT_SENSOR_WIDTH);
copy_v3_v3(state->prev_eye_position_ofs, draw_data->eye_position_ofs);
memcpy(&state->prev_base_pose, &draw_data->base_pose, sizeof(state->prev_base_pose));
state->prev_base_scale = draw_data->base_scale;
memcpy(&state->prev_base_pose, &state->base_pose, sizeof(state->prev_base_pose));
state->prev_base_scale = state->base_scale;
memcpy(&state->prev_local_pose, &draw_view->local_pose, sizeof(state->prev_local_pose));
copy_v3_v3(state->prev_eye_position_ofs, draw_data->eye_position_ofs);
@ -424,6 +439,11 @@ wmXrSessionState *WM_xr_session_state_handle_get(const wmXrData *xr)
return xr->runtime ? &xr->runtime->session_state : nullptr;
}
wmXrViewfinderState *WM_xr_session_state_viewfinder_handle_get(const wmXrData *xr)
{
return xr->runtime ? &xr->runtime->session_state.viewfinder : nullptr;
}
bContext *WM_xr_session_context_ensure(wmXrData *xr, const wmWindowManager *wm)
{
if (xr->runtime == nullptr) {
@ -472,6 +492,220 @@ bool WM_xr_session_state_viewer_pose_rotation_get(const wmXrData *xr, float r_ro
return true;
}
bool WM_xr_session_state_viewfinder_location_get(const wmXrData *xr, float r_location[3])
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
zero_v3(r_location);
return false;
}
copy_v3_v3(r_location, xr->runtime->session_state.viewfinder.capture_position);
return true;
}
bool WM_xr_session_state_viewfinder_orientation_get(const wmXrData *xr, float r_rotation[4])
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
unit_qt(r_rotation);
return false;
}
copy_v4_v4(r_rotation, xr->runtime->session_state.viewfinder.capture_orientation_quat);
return true;
}
void WM_xr_session_state_viewfinder_trigger_flash(wmXrData *xr)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.last_flash_trigger_time = BLI_time_now_seconds();
}
}
void WM_xr_session_state_viewfinder_trigger_focus_indicator(wmXrData *xr, bool hit_success)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.last_focus_hit_time = BLI_time_now_seconds();
xr->runtime->session_state.viewfinder.last_focus_hit_success = hit_success;
}
}
void WM_xr_session_state_viewfinder_reset_view_smoothing(wmXrData *xr)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.smoothing_delta_t = 0.0;
}
}
bool WM_xr_session_state_viewfinder_capture_dof_enabled_get(const wmXrData *xr,
bool *r_dof_enabled)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_dof_enabled = 0.0f;
return false;
}
*r_dof_enabled = xr->runtime->session_state.viewfinder.capture_dof_enabled;
return true;
}
void WM_xr_session_state_viewfinder_capture_dof_enabled_set(wmXrData *xr, bool dof_enabled)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.capture_dof_enabled = dof_enabled;
}
}
bool WM_xr_session_state_viewfinder_capture_lens_focal_get(const wmXrData *xr, float *r_lens_focal)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_lens_focal = 1.0f;
return false;
}
*r_lens_focal = xr->runtime->session_state.viewfinder.capture_lens_focal;
return true;
}
void WM_xr_session_state_viewfinder_capture_lens_focal_set(wmXrData *xr, float lens_focal)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.capture_lens_focal = lens_focal;
}
}
bool WM_xr_session_state_viewfinder_capture_dof_distance_get(const wmXrData *xr,
float *r_dof_distance)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_dof_distance = 1.0f;
return false;
}
*r_dof_distance = xr->runtime->session_state.viewfinder.capture_dof_distance;
return true;
}
void WM_xr_session_state_viewfinder_capture_dof_distance_set(wmXrData *xr, float dof_distance)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.capture_dof_distance = dof_distance;
}
}
bool WM_xr_session_state_viewfinder_capture_dof_fstop_get(const wmXrData *xr, float *r_dof_fstop)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_dof_fstop = 1.0f;
return false;
}
*r_dof_fstop = xr->runtime->session_state.viewfinder.capture_dof_fstop;
return true;
}
void WM_xr_session_state_viewfinder_capture_dof_fstop_set(wmXrData *xr, float dof_fstop)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.capture_dof_fstop = dof_fstop;
}
}
bool WM_xr_session_state_viewfinder_playback_show_active_capture_in_space_enabled_get(
const wmXrData *xr, bool *r_enabled)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_enabled = 0.0f;
return false;
}
*r_enabled = xr->runtime->session_state.viewfinder.playback_show_active_capture_in_space_enabled;
return true;
}
void WM_xr_session_state_viewfinder_playback_show_active_capture_in_space_enabled_set(wmXrData *xr,
bool enabled)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.playback_show_active_capture_in_space_enabled = enabled;
}
}
bool WM_xr_session_state_viewfinder_active_mode_get(const wmXrData *xr, eXrViewfinderMode *r_mode)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_mode = static_cast<eXrViewfinderMode>(0);
return false;
}
*r_mode = xr->runtime->session_state.viewfinder.active_mode;
return true;
}
void WM_xr_session_state_viewfinder_active_mode_set(wmXrData *xr, eXrViewfinderMode mode)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.active_mode = mode;
}
}
bool WM_xr_session_state_viewfinder_active_action_live_get(const wmXrData *xr,
eXrViewfinderLiveAction *r_action)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_action = static_cast<eXrViewfinderLiveAction>(0);
return false;
}
*r_action = xr->runtime->session_state.viewfinder.active_action_live;
return true;
}
void WM_xr_session_state_viewfinder_active_action_live_set(wmXrData *xr,
eXrViewfinderLiveAction action)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.active_action_live = action;
}
}
bool WM_xr_session_state_viewfinder_active_action_playback_get(
const wmXrData *xr, eXrViewfinderPlaybackAction *r_action)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_action = static_cast<eXrViewfinderPlaybackAction>(0);
return false;
}
*r_action = xr->runtime->session_state.viewfinder.active_action_playback;
return true;
}
void WM_xr_session_state_viewfinder_active_action_playback_set(wmXrData *xr,
eXrViewfinderPlaybackAction action)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.active_action_playback = action;
}
}
bool WM_xr_session_state_viewfinder_active_action_confirm_get(const wmXrData *xr,
eXrViewfinderConfirmAction *r_action)
{
if (!WM_xr_session_is_ready(xr) || !xr->runtime->session_state.is_view_data_set) {
*r_action = static_cast<eXrViewfinderConfirmAction>(0);
return false;
}
*r_action = xr->runtime->session_state.viewfinder.active_action_confirm;
return true;
}
void WM_xr_session_state_viewfinder_active_action_confirm_set(wmXrData *xr,
eXrViewfinderConfirmAction action)
{
if (WM_xr_session_exists(xr)) {
xr->runtime->session_state.viewfinder.active_action_confirm = action;
}
}
bool WM_xr_session_state_viewer_pose_matrix_info_get(const wmXrData *xr,
float r_viewmat[4][4],
float *r_focal_len)
@ -641,6 +875,45 @@ void WM_xr_session_state_navigation_reset(wmXrSessionState *state)
state->swap_hands = false;
}
void WM_xr_session_state_viewfinder_reset(wmXrSessionState *state)
{
/* Runtime values. */
state->viewfinder.smoothing_delta_t = 0.0;
state->viewfinder.last_flash_trigger_time = 0.0;
state->viewfinder.last_focus_hit_time = 0.0;
state->viewfinder.last_focus_hit_success = false;
/* Create a Camera data ID to override the View3D camera (for setting parameters such as DoF).
* This ID is freed in #wm_xr_session_data_free. */
if (state->viewfinder.render_cam_data_id == nullptr) {
state->viewfinder.render_cam_data_id = BKE_id_new_nomain<Camera>("ViewfinderCamera");
}
/* Create a Camera logo texture to draw on the backside of the viewfinder. */
if (state->viewfinder.backside_logo_texture == nullptr) {
ImBuf *ibuf = ui::svg_icon_bitmap(ICON_RESTRICT_RENDER_OFF, 256.0f, false);
if (ibuf) {
state->viewfinder.backside_logo_texture = IMB_create_gpu_texture(
"viewfinder_backside_logo", ibuf, false, true, true);
IMB_freeImBuf(ibuf);
}
}
/* Capture settings. */
state->viewfinder.capture_dof_enabled = false;
state->viewfinder.capture_lens_focal = 50.0f;
state->viewfinder.capture_dof_fstop = 2.8f;
state->viewfinder.capture_dof_distance = 10.0f;
/* Playback settings. */
state->viewfinder.playback_show_active_capture_in_space_enabled = true;
/* Active modes. */
state->viewfinder.active_mode = XR_VIEWFINDER_MODE_LIVE;
state->viewfinder.active_action_live = XR_VIEWFINDER_ACTION_LIVE_LENS;
state->viewfinder.active_action_playback = XR_VIEWFINDER_ACTION_PB_BROWSE;
}
void WM_xr_session_state_vignette_activate(wmXrData *xr)
{
if (WM_xr_session_exists(xr)) {

View file

@ -27,4 +27,22 @@ bool wm_xr_events_handle(wmWindowManager *wm);
void wm_xr_operatortypes_register();
/* `wm_xr_location_scouting.cc` */
/* NOTE: Keep in sync with the Python VR Scene Inspection add-on VRCapture class.
* See comment in #wm_xr_location_scouting_get_active_capture. */
struct XrLocationScoutingCapture {
float3 position;
float4 orientation_quat;
float lens_focal;
bool dof_enabled;
float dof_distance;
float dof_fstop;
};
bool wm_xr_location_scouting_is_captures_empty(Scene *scene);
std::optional<XrLocationScoutingCapture> wm_xr_location_scouting_get_active_capture(Scene *scene);
} // namespace blender