# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # import bpy from bpy.types import ( Gizmo, GizmoGroup, PropertyGroup, UIList, Menu, Panel, Operator, ) from bpy.props import ( CollectionProperty, IntProperty, BoolProperty, ) from bpy.app.handlers import persistent bl_info = { "name": "VR Scene Inspection", "author": "Julian Eisel (Severin), Sebastian Koenig", "version": (0, 9, 0), "blender": (2, 90, 0), "location": "3D View > Sidebar > VR", "description": ("View the viewport with virtual reality glasses " "(head-mounted displays)"), "support": "OFFICIAL", "warning": "This is an early, limited preview of in development " "VR support for Blender.", "doc_url": "{BLENDER_MANUAL_URL}/addons/3d_view/vr_scene_inspection.html", "category": "3D View", } @persistent def ensure_default_vr_landmark(context: bpy.context): # Ensure there's a default landmark (scene camera by default). landmarks = bpy.context.scene.vr_landmarks if not landmarks: landmarks.add() landmarks[0].type = 'SCENE_CAMERA' def xr_landmark_active_type_update(self, context): wm = context.window_manager session_settings = wm.xr_session_settings landmark_active = VRLandmark.get_active_landmark(context) # Update session's base pose type to the matching type. if landmark_active.type == 'SCENE_CAMERA': session_settings.base_pose_type = 'SCENE_CAMERA' elif landmark_active.type == 'USER_CAMERA': session_settings.base_pose_type = 'OBJECT' elif landmark_active.type == 'CUSTOM': session_settings.base_pose_type = 'CUSTOM' def xr_landmark_active_camera_update(self, context): session_settings = context.window_manager.xr_session_settings landmark_active = VRLandmark.get_active_landmark(context) # Update the anchor object to the (new) camera of this landmark. session_settings.base_pose_object = landmark_active.base_pose_camera def xr_landmark_active_base_pose_location_update(self, context): session_settings = context.window_manager.xr_session_settings landmark_active = VRLandmark.get_active_landmark(context) session_settings.base_pose_location = landmark_active.base_pose_location def xr_landmark_active_base_pose_angle_update(self, context): session_settings = context.window_manager.xr_session_settings landmark_active = VRLandmark.get_active_landmark(context) session_settings.base_pose_angle = landmark_active.base_pose_angle def xr_landmark_type_update(self, context): landmark_selected = VRLandmark.get_selected_landmark(context) landmark_active = VRLandmark.get_active_landmark(context) # Only update session settings data if the changed landmark is actually # the active one. if landmark_active == landmark_selected: xr_landmark_active_type_update(self, context) def xr_landmark_camera_update(self, context): landmark_selected = VRLandmark.get_selected_landmark(context) landmark_active = VRLandmark.get_active_landmark(context) # Only update session settings data if the changed landmark is actually # the active one. if landmark_active == landmark_selected: xr_landmark_active_camera_update(self, context) def xr_landmark_base_pose_location_update(self, context): landmark_selected = VRLandmark.get_selected_landmark(context) landmark_active = VRLandmark.get_active_landmark(context) # Only update session settings data if the changed landmark is actually # the active one. if landmark_active == landmark_selected: xr_landmark_active_base_pose_location_update(self, context) def xr_landmark_base_pose_angle_update(self, context): landmark_selected = VRLandmark.get_selected_landmark(context) landmark_active = VRLandmark.get_active_landmark(context) # Only update session settings data if the changed landmark is actually # the active one. if landmark_active == landmark_selected: xr_landmark_active_base_pose_angle_update(self, context) def xr_landmark_camera_object_poll(self, object): return object.type == 'CAMERA' def xr_landmark_active_update(self, context): wm = context.window_manager xr_landmark_active_type_update(self, context) xr_landmark_active_camera_update(self, context) xr_landmark_active_base_pose_location_update(self, context) xr_landmark_active_base_pose_angle_update(self, context) if wm.xr_session_state: wm.xr_session_state.reset_to_base_pose(context) class VIEW3D_MT_landmark_menu(Menu): bl_label = "Landmark Controls" def draw(self, _context): layout = self.layout layout.operator("view3d.vr_landmark_from_camera") layout.operator("view3d.update_vr_landmark") layout.separator() layout.operator("view3d.cursor_to_vr_landmark") layout.operator("view3d.camera_to_vr_landmark") layout.operator("view3d.add_camera_from_vr_landmark") class VRLandmark(PropertyGroup): name: bpy.props.StringProperty( name="VR Landmark", default="Landmark" ) type: bpy.props.EnumProperty( name="Type", items=[ ('SCENE_CAMERA', "Scene Camera", "Use scene's currently active camera to define the VR view base " "location and rotation"), ('USER_CAMERA', "Custom Camera", "Use an existing camera to define the VR view base location and " "rotation"), ('CUSTOM', "Custom Pose", "Allow a manually defined position and rotation to be used as " "the VR view base pose"), ], default='SCENE_CAMERA', update=xr_landmark_type_update, ) base_pose_camera: bpy.props.PointerProperty( name="Camera", type=bpy.types.Object, poll=xr_landmark_camera_object_poll, update=xr_landmark_camera_update, ) base_pose_location: bpy.props.FloatVectorProperty( name="Base Pose Location", subtype='TRANSLATION', update=xr_landmark_base_pose_location_update, ) base_pose_angle: bpy.props.FloatProperty( name="Base Pose Angle", subtype='ANGLE', update=xr_landmark_base_pose_angle_update, ) @staticmethod def get_selected_landmark(context): scene = context.scene landmarks = scene.vr_landmarks return ( None if (len(landmarks) < 1) else landmarks[scene.vr_landmarks_selected] ) @staticmethod def get_active_landmark(context): scene = context.scene landmarks = scene.vr_landmarks return ( None if (len(landmarks) < 1) else landmarks[scene.vr_landmarks_active] ) class VIEW3D_UL_vr_landmarks(UIList): def draw_item(self, context, layout, _data, item, icon, _active_data, _active_propname, index): landmark = item landmark_active_idx = context.scene.vr_landmarks_active layout.emboss = 'NONE' layout.prop(landmark, "name", text="") icon = ( 'RADIOBUT_ON' if (index == landmark_active_idx) else 'RADIOBUT_OFF' ) props = layout.operator( "view3d.vr_landmark_activate", text="", icon=icon) props.index = index class VIEW3D_PT_vr_landmarks(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" bl_label = "Landmarks" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout scene = context.scene landmark_selected = VRLandmark.get_selected_landmark(context) layout.use_property_split = True layout.use_property_decorate = False # No animation. row = layout.row() row.template_list("VIEW3D_UL_vr_landmarks", "", scene, "vr_landmarks", scene, "vr_landmarks_selected", rows=3) col = row.column(align=True) col.operator("view3d.vr_landmark_add", icon='ADD', text="") col.operator("view3d.vr_landmark_remove", icon='REMOVE', text="") col.operator("view3d.vr_landmark_from_session", icon='PLUS', text="") col.menu("VIEW3D_MT_landmark_menu", icon='DOWNARROW_HLT', text="") if landmark_selected: layout.prop(landmark_selected, "type") if landmark_selected.type == 'USER_CAMERA': layout.prop(landmark_selected, "base_pose_camera") elif landmark_selected.type == 'CUSTOM': layout.prop(landmark_selected, "base_pose_location", text="Location") layout.prop(landmark_selected, "base_pose_angle", text="Angle") class VIEW3D_PT_vr_session_view(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" bl_label = "View" def draw(self, context): layout = self.layout session_settings = context.window_manager.xr_session_settings layout.use_property_split = True layout.use_property_decorate = False # No animation. col = layout.column(align=True, heading="Show") col.prop(session_settings, "show_floor", text="Floor") col.prop(session_settings, "show_annotation", text="Annotations") col = layout.column(align=True) col.prop(session_settings, "clip_start", text="Clip Start") col.prop(session_settings, "clip_end", text="End") class VIEW3D_PT_vr_session(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" bl_label = "VR Session" def draw(self, context): layout = self.layout session_settings = context.window_manager.xr_session_settings layout.use_property_split = True layout.use_property_decorate = False # No animation. is_session_running = bpy.types.XrSessionState.is_running(context) # Using SNAP_FACE because it looks like a stop icon -- I shouldn't # have commit rights... toggle_info = ( ("Start VR Session", 'PLAY') if not is_session_running else ( "Stop VR Session", 'SNAP_FACE') ) layout.operator("wm.xr_session_toggle", text=toggle_info[0], icon=toggle_info[1]) layout.separator() layout.prop(session_settings, "use_positional_tracking") class VIEW3D_PT_vr_info(bpy.types.Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" bl_label = "VR Info" @classmethod def poll(cls, context): return not bpy.app.build_options.xr_openxr def draw(self, context): layout = self.layout layout.label(icon='ERROR', text="Built without VR/OpenXR features") class VIEW3D_OT_vr_landmark_add(Operator): bl_idname = "view3d.vr_landmark_add" bl_label = "Add VR Landmark" bl_description = "Add a new VR landmark to the list and select it" bl_options = {'UNDO', 'REGISTER'} def execute(self, context): scene = context.scene landmarks = scene.vr_landmarks landmarks.add() # select newly created set scene.vr_landmarks_selected = len(landmarks) - 1 return {'FINISHED'} class VIEW3D_OT_vr_landmark_from_camera(Operator): bl_idname = "view3d.vr_landmark_from_camera" bl_label = "Add VR Landmark from camera" bl_description = "Add a new VR landmark from the active camera object to the list and select it" bl_options = {'UNDO', 'REGISTER'} @classmethod def poll(cls, context): cam_selected = False vl_objects = bpy.context.view_layer.objects if vl_objects.active and vl_objects.active.type == 'CAMERA': cam_selected = True return cam_selected def execute(self, context): scene = context.scene landmarks = scene.vr_landmarks cam = context.view_layer.objects.active lm = landmarks.add() lm.type = 'USER_CAMERA' lm.base_pose_camera = cam lm.name = "LM_" + cam.name # select newly created set scene.vr_landmarks_selected = len(landmarks) - 1 return {'FINISHED'} class VIEW3D_OT_vr_landmark_from_session(Operator): bl_idname = "view3d.vr_landmark_from_session" bl_label = "Add VR Landmark from session" bl_description = "Add VR landmark from the viewer pose of the running VR session to the list and select it" bl_options = {'UNDO', 'REGISTER'} @classmethod def poll(cls, context): return bpy.types.XrSessionState.is_running(context) @staticmethod def _calc_landmark_angle_from_viewer_rotation(rot): from mathutils import Vector # We want an angle around Z based on the current viewer rotation. Idea # is to create a vector from the viewer rotation, project that onto a # Z-Up plane and use the resulting vector to get an angle around Z. view_rot_vec = Vector((0, 0, 1)) view_rot_vec.rotate(rot) angle_vec = view_rot_vec - view_rot_vec.project(Vector((0, 0, 1))) # We could probably use a 3D version of Vector.angle_signed() here, but # that's not available. So manually calculate it via a quaternion delta. forward_vec = Vector((0, -1, 0)) diff = angle_vec.rotation_difference(forward_vec) return diff.angle * -diff.axis[2] def execute(self, context): scene = context.scene landmarks = scene.vr_landmarks wm = context.window_manager lm = landmarks.add() lm.type = 'CUSTOM' scene.vr_landmarks_selected = len(landmarks) - 1 loc = wm.xr_session_state.viewer_pose_location rot = wm.xr_session_state.viewer_pose_rotation angle = self._calc_landmark_angle_from_viewer_rotation(rot) lm.base_pose_location = loc lm.base_pose_angle = angle return {'FINISHED'} class VIEW3D_OT_update_vr_landmark(Operator): bl_idname = "view3d.update_vr_landmark" bl_label = "Update Custom VR Landmark" bl_description = "Update the selected landmark from the current viewer pose in the VR session" bl_options = {'UNDO', 'REGISTER'} @classmethod def poll(cls, context): selected_landmark = VRLandmark.get_selected_landmark(context) return bpy.types.XrSessionState.is_running(context) and selected_landmark.type == 'CUSTOM' def execute(self, context): wm = context.window_manager lm = VRLandmark.get_selected_landmark(context) loc = wm.xr_session_state.viewer_pose_location rot = wm.xr_session_state.viewer_pose_rotation.to_euler() lm.base_pose_location = loc lm.base_pose_angle = rot # Re-activate the landmark to trigger viewer reset and flush landmark settings to the session settings. xr_landmark_active_update(None, context) return {'FINISHED'} class VIEW3D_OT_vr_landmark_remove(Operator): bl_idname = "view3d.vr_landmark_remove" bl_label = "Remove VR Landmark" bl_description = "Delete the selected VR landmark from the list" bl_options = {'UNDO', 'REGISTER'} def execute(self, context): scene = context.scene landmarks = scene.vr_landmarks if len(landmarks) > 1: landmark_selected_idx = scene.vr_landmarks_selected landmarks.remove(landmark_selected_idx) scene.vr_landmarks_selected -= 1 return {'FINISHED'} class VIEW3D_OT_cursor_to_vr_landmark(Operator): bl_idname = "view3d.cursor_to_vr_landmark" bl_label = "Cursor to VR Landmark" bl_description = "Move the 3D Cursor to the selected VR Landmark" bl_options = {'UNDO', 'REGISTER'} @classmethod def poll(cls, context): lm = VRLandmark.get_selected_landmark(context) if lm.type == 'SCENE_CAMERA': return context.scene.camera is not None elif lm.type == 'USER_CAMERA': return lm.base_pose_camera is not None return True def execute(self, context): scene = context.scene lm = VRLandmark.get_selected_landmark(context) if lm.type == 'SCENE_CAMERA': lm_pos = scene.camera.location elif lm.type == 'USER_CAMERA': lm_pos = lm.base_pose_camera.location else: lm_pos = lm.base_pose_location scene.cursor.location = lm_pos return{'FINISHED'} class VIEW3d_OT_add_camera_from_vr_landmark(Operator): bl_idname = "view3d.add_camera_from_vr_landmark" bl_label = "New Camera from VR Landmark" bl_description = "Create a new Camera from the selected VR Landmark" bl_options = {'UNDO', 'REGISTER'} def execute(self, context): import math scene = context.scene lm = VRLandmark.get_selected_landmark(context) cam = bpy.data.cameras.new("Camera_" + lm.name) new_cam = bpy.data.objects.new("Camera_" + lm.name, cam) scene.collection.objects.link(new_cam) angle = lm.base_pose_angle new_cam.location = lm.base_pose_location new_cam.rotation_euler = (math.pi, 0, angle) return {'FINISHED'} class VIEW3D_OT_camera_to_vr_landmark(Operator): bl_idname = "view3d.camera_to_vr_landmark" bl_label = "Scene Camera to VR Landmark" bl_description = "Position the scene camera at the selected landmark" bl_options = {'UNDO', 'REGISTER'} @classmethod def poll(cls, context): return context.scene.camera is not None def execute(self, context): import math scene = context.scene lm = VRLandmark.get_selected_landmark(context) cam = scene.camera angle = lm.base_pose_angle cam.location = lm.base_pose_location cam.rotation_euler = (math.pi / 2, 0, angle) return {'FINISHED'} class VIEW3D_OT_vr_landmark_activate(Operator): bl_idname = "view3d.vr_landmark_activate" bl_label = "Activate VR Landmark" bl_description = "Change to the selected VR landmark from the list" bl_options = {'UNDO', 'REGISTER'} index: IntProperty( name="Index", options={'HIDDEN'}, ) def execute(self, context): scene = context.scene if self.index >= len(scene.vr_landmarks): return {'CANCELLED'} scene.vr_landmarks_active = ( self.index if self.properties.is_property_set( "index") else scene.vr_landmarks_selected ) return {'FINISHED'} class VIEW3D_PT_vr_viewport_feedback(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" bl_label = "Viewport Feedback" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout view3d = context.space_data col = layout.column(align=True) col.label(icon='ERROR', text="Note:") col.label(text="Settings here may have a significant") col.label(text="performance impact!") layout.separator() layout.prop(view3d.shading, "vr_show_virtual_camera") layout.prop(view3d.shading, "vr_show_landmarks") layout.prop(view3d, "mirror_xr_session") class VIEW3D_GT_vr_camera_cone(Gizmo): bl_idname = "VIEW_3D_GT_vr_camera_cone" aspect = 1.0, 1.0 def draw(self, context): import bgl if not hasattr(self, "frame_shape"): aspect = self.aspect 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), ) lines_shape_verts = ( (0.0, 0.0, 0.0), frame_shape_verts[0], (0.0, 0.0, 0.0), frame_shape_verts[1], (0.0, 0.0, 0.0), frame_shape_verts[2], (0.0, 0.0, 0.0), frame_shape_verts[3], ) self.frame_shape = self.new_custom_shape( 'LINE_LOOP', frame_shape_verts) self.lines_shape = self.new_custom_shape( 'LINES', lines_shape_verts) # Ensure correct GL state (otherwise other gizmos might mess that up) bgl.glLineWidth(1) bgl.glEnable(bgl.GL_BLEND) self.draw_custom_shape(self.frame_shape) self.draw_custom_shape(self.lines_shape) class VIEW3D_GGT_vr_viewer_pose(GizmoGroup): bl_idname = "VIEW3D_GGT_vr_viewer_pose" bl_label = "VR Viewer Pose Indicator" bl_space_type = 'VIEW_3D' bl_region_type = 'WINDOW' bl_options = {'3D', 'PERSISTENT', 'SCALE', 'VR_REDRAWS'} @classmethod def poll(cls, context): view3d = context.space_data return ( view3d.shading.vr_show_virtual_camera and bpy.types.XrSessionState.is_running(context) and not view3d.mirror_xr_session ) @staticmethod def _get_viewer_pose_matrix(context): from mathutils import Matrix, Quaternion wm = context.window_manager loc = wm.xr_session_state.viewer_pose_location rot = wm.xr_session_state.viewer_pose_rotation rotmat = Matrix.Identity(3) rotmat.rotate(rot) rotmat.resize_4x4() transmat = Matrix.Translation(loc) return transmat @ rotmat def setup(self, context): gizmo = self.gizmos.new(VIEW3D_GT_vr_camera_cone.bl_idname) gizmo.aspect = 1 / 3, 1 / 4 gizmo.color = gizmo.color_highlight = 0.2, 0.6, 1.0 gizmo.alpha = 1.0 self.gizmo = gizmo def draw_prepare(self, context): self.gizmo.matrix_basis = self._get_viewer_pose_matrix(context) class VIEW3D_GGT_vr_landmarks(GizmoGroup): bl_idname = "VIEW3D_GGT_vr_landmarks" bl_label = "VR Landmark Indicators" bl_space_type = 'VIEW_3D' bl_region_type = 'WINDOW' bl_options = {'3D', 'PERSISTENT', 'SCALE'} @classmethod def poll(cls, context): view3d = context.space_data return ( view3d.shading.vr_show_landmarks ) def setup(self, context): pass def draw_prepare(self, context): # first delete the old gizmos for g in self.gizmos: self.gizmos.remove(g) from math import radians from mathutils import Matrix, Euler scene = context.scene landmarks = scene.vr_landmarks for lm in landmarks: if ((lm.type == 'SCENE_CAMERA' and not scene.camera) or (lm.type == 'USER_CAMERA' and not lm.base_pose_camera)): continue gizmo = self.gizmos.new(VIEW3D_GT_vr_camera_cone.bl_idname) gizmo.aspect = 1 / 3, 1 / 4 gizmo.color = gizmo.color_highlight = 0.2, 1.0, 0.6 gizmo.alpha = 1.0 self.gizmo = gizmo if lm.type == 'SCENE_CAMERA': cam = scene.camera lm_mat = cam.matrix_world if cam else Matrix.Identity(4) elif lm.type == 'USER_CAMERA': lm_mat = lm.base_pose_camera.matrix_world else: angle = lm.base_pose_angle raw_rot = Euler((radians(90.0), 0, angle)) rotmat = Matrix.Identity(3) rotmat.rotate(raw_rot) rotmat.resize_4x4() transmat = Matrix.Translation(lm.base_pose_location) lm_mat = transmat @ rotmat self.gizmo.matrix_basis = lm_mat classes = ( VIEW3D_PT_vr_session, VIEW3D_PT_vr_session_view, VIEW3D_PT_vr_landmarks, VIEW3D_PT_vr_viewport_feedback, VRLandmark, VIEW3D_UL_vr_landmarks, VIEW3D_MT_landmark_menu, VIEW3D_OT_vr_landmark_add, VIEW3D_OT_vr_landmark_remove, VIEW3D_OT_vr_landmark_activate, VIEW3D_OT_vr_landmark_from_session, VIEW3d_OT_add_camera_from_vr_landmark, VIEW3D_OT_camera_to_vr_landmark, VIEW3D_OT_vr_landmark_from_camera, VIEW3D_OT_cursor_to_vr_landmark, VIEW3D_OT_update_vr_landmark, VIEW3D_GT_vr_camera_cone, VIEW3D_GGT_vr_viewer_pose, VIEW3D_GGT_vr_landmarks, ) def register(): if not bpy.app.build_options.xr_openxr: bpy.utils.register_class(VIEW3D_PT_vr_info) return for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.vr_landmarks = CollectionProperty( name="Landmark", type=VRLandmark, ) bpy.types.Scene.vr_landmarks_selected = IntProperty( name="Selected Landmark" ) bpy.types.Scene.vr_landmarks_active = IntProperty( update=xr_landmark_active_update, ) # 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 = BoolProperty( name="Show VR Camera" ) bpy.types.View3DShading.vr_show_landmarks = BoolProperty( name="Show Landmarks" ) bpy.app.handlers.load_post.append(ensure_default_vr_landmark) def unregister(): if not bpy.app.build_options.xr_openxr: bpy.utils.unregister_class(VIEW3D_PT_vr_info) return for cls in classes: bpy.utils.unregister_class(cls) del bpy.types.Scene.vr_landmarks del bpy.types.Scene.vr_landmarks_selected del bpy.types.Scene.vr_landmarks_active del bpy.types.View3DShading.vr_show_virtual_camera del bpy.types.View3DShading.vr_show_landmarks bpy.app.handlers.load_post.remove(ensure_default_vr_landmark) if __name__ == "__main__": register()