Fix T75470: File overwrite warning for UV Layout export
[blender-addons.git] / viewport_vr_preview.py
blob1f8bbb64e92d5b4b0ea6448ccb5bff018f33673f
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 import bpy
22 from bpy.types import (
23 Gizmo,
24 GizmoGroup,
26 from bpy.props import (
27 CollectionProperty,
28 IntProperty,
29 BoolProperty,
31 from bpy.app.handlers import persistent
33 bl_info = {
34 "name": "VR Scene Inspection",
35 "author": "Julian Eisel (Severin)",
36 "version": (0, 1, 0),
37 "blender": (2, 83, 8),
38 "location": "3D View > Sidebar > VR",
39 "description": ("View the viewport with virtual reality glasses "
40 "(head-mounted displays)"),
41 "support": "OFFICIAL",
42 "warning": "This is an early, limited preview of in development "
43 "VR support for Blender.",
44 "doc_url": "{BLENDER_MANUAL_URL}/addons/3d_view/vr_scene_inspection.html",
45 "category": "3D View",
49 @persistent
50 def ensure_default_vr_landmark(context: bpy.context):
51 # Ensure there's a default landmark (scene camera by default).
52 landmarks = bpy.context.scene.vr_landmarks
53 if not landmarks:
54 landmarks.add()
55 landmarks[0].type = 'SCENE_CAMERA'
58 def xr_landmark_active_type_update(self, context):
59 wm = context.window_manager
60 session_settings = wm.xr_session_settings
61 landmark_active = VRLandmark.get_active_landmark(context)
63 # Update session's base pose type to the matching type.
64 if landmark_active.type == 'SCENE_CAMERA':
65 session_settings.base_pose_type = 'SCENE_CAMERA'
66 elif landmark_active.type == 'USER_CAMERA':
67 session_settings.base_pose_type = 'OBJECT'
68 # elif landmark_active.type == 'CUSTOM':
69 # session_settings.base_pose_type = 'CUSTOM'
72 def xr_landmark_active_camera_update(self, context):
73 session_settings = context.window_manager.xr_session_settings
74 landmark_active = VRLandmark.get_active_landmark(context)
76 # Update the anchor object to the (new) camera of this landmark.
77 session_settings.base_pose_object = landmark_active.base_pose_camera
80 def xr_landmark_active_base_pose_location_update(self, context):
81 session_settings = context.window_manager.xr_session_settings
82 landmark_active = VRLandmark.get_active_landmark(context)
84 session_settings.base_pose_location = landmark_active.base_pose_location
87 def xr_landmark_active_base_pose_angle_update(self, context):
88 session_settings = context.window_manager.xr_session_settings
89 landmark_active = VRLandmark.get_active_landmark(context)
91 session_settings.base_pose_angle = landmark_active.base_pose_angle
94 def xr_landmark_type_update(self, context):
95 landmark_selected = VRLandmark.get_selected_landmark(context)
96 landmark_active = VRLandmark.get_active_landmark(context)
98 # Only update session settings data if the changed landmark is actually
99 # the active one.
100 if landmark_active == landmark_selected:
101 xr_landmark_active_type_update(self, context)
104 def xr_landmark_camera_update(self, context):
105 landmark_selected = VRLandmark.get_selected_landmark(context)
106 landmark_active = VRLandmark.get_active_landmark(context)
108 # Only update session settings data if the changed landmark is actually
109 # the active one.
110 if landmark_active == landmark_selected:
111 xr_landmark_active_camera_update(self, context)
114 def xr_landmark_base_pose_location_update(self, context):
115 landmark_selected = VRLandmark.get_selected_landmark(context)
116 landmark_active = VRLandmark.get_active_landmark(context)
118 # Only update session settings data if the changed landmark is actually
119 # the active one.
120 if landmark_active == landmark_selected:
121 xr_landmark_active_base_pose_location_update(self, context)
124 def xr_landmark_base_pose_angle_update(self, context):
125 landmark_selected = VRLandmark.get_selected_landmark(context)
126 landmark_active = VRLandmark.get_active_landmark(context)
128 # Only update session settings data if the changed landmark is actually
129 # the active one.
130 if landmark_active == landmark_selected:
131 xr_landmark_active_base_pose_angle_update(self, context)
134 def xr_landmark_camera_object_poll(self, object):
135 return object.type == 'CAMERA'
138 def xr_landmark_active_update(self, context):
139 xr_landmark_active_type_update(self, context)
140 xr_landmark_active_camera_update(self, context)
141 xr_landmark_active_base_pose_location_update(self, context)
142 xr_landmark_active_base_pose_angle_update(self, context)
145 class VRLandmark(bpy.types.PropertyGroup):
146 name: bpy.props.StringProperty(
147 name="VR Landmark",
148 default="Landmark"
150 type: bpy.props.EnumProperty(
151 name="Type",
152 items=[
153 ('SCENE_CAMERA', "Scene Camera",
154 "Use scene's currently active camera to define the VR view base "
155 "location and rotation"),
156 ('USER_CAMERA', "Custom Camera",
157 "Use an existing camera to define the VR view base location and "
158 "rotation"),
159 # Custom base poses work, but it's uncertain if they are really
160 # needed. Disabled for now.
161 # ('CUSTOM', "Custom Pose",
162 # "Allow a manually definied position and rotation to be used as "
163 # "the VR view base pose"),
165 default='SCENE_CAMERA',
166 update=xr_landmark_type_update,
168 base_pose_camera: bpy.props.PointerProperty(
169 name="Camera",
170 type=bpy.types.Object,
171 poll=xr_landmark_camera_object_poll,
172 update=xr_landmark_camera_update,
174 base_pose_location: bpy.props.FloatVectorProperty(
175 name="Base Pose Location",
176 subtype='TRANSLATION',
177 update=xr_landmark_base_pose_location_update,
180 base_pose_angle: bpy.props.FloatProperty(
181 name="Base Pose Angle",
182 subtype='ANGLE',
183 update=xr_landmark_base_pose_angle_update,
186 @staticmethod
187 def get_selected_landmark(context):
188 scene = context.scene
189 landmarks = scene.vr_landmarks
191 return (
192 None if (len(landmarks) <
193 1) else landmarks[scene.vr_landmarks_selected]
196 @staticmethod
197 def get_active_landmark(context):
198 scene = context.scene
199 landmarks = scene.vr_landmarks
201 return (
202 None if (len(landmarks) <
203 1) else landmarks[scene.vr_landmarks_active]
207 class VIEW3D_UL_vr_landmarks(bpy.types.UIList):
208 def draw_item(self, context, layout, _data, item, icon, _active_data,
209 _active_propname, index):
210 landmark = item
211 landmark_active_idx = context.scene.vr_landmarks_active
213 layout.emboss = 'NONE'
215 layout.prop(landmark, "name", text="")
217 icon = 'SOLO_ON' if (index == landmark_active_idx) else 'SOLO_OFF'
218 props = layout.operator(
219 "view3d.vr_landmark_activate", text="", icon=icon)
220 props.index = index
223 class VIEW3D_PT_vr_landmarks(bpy.types.Panel):
224 bl_space_type = 'VIEW_3D'
225 bl_region_type = 'UI'
226 bl_category = "VR"
227 bl_label = "Landmarks"
228 bl_options = {'DEFAULT_CLOSED'}
230 def draw(self, context):
231 layout = self.layout
232 scene = context.scene
233 landmark_selected = VRLandmark.get_selected_landmark(context)
235 layout.use_property_split = True
236 layout.use_property_decorate = False # No animation.
238 row = layout.row()
240 row.template_list("VIEW3D_UL_vr_landmarks", "", scene, "vr_landmarks",
241 scene, "vr_landmarks_selected", rows=3)
243 col = row.column(align=True)
244 col.operator("view3d.vr_landmark_add", icon='ADD', text="")
245 col.operator("view3d.vr_landmark_remove", icon='REMOVE', text="")
247 if landmark_selected:
248 layout.prop(landmark_selected, "type")
250 if landmark_selected.type == 'USER_CAMERA':
251 layout.prop(landmark_selected, "base_pose_camera")
252 # elif landmark_selected.type == 'CUSTOM':
253 # layout.prop(landmark_selected,
254 # "base_pose_location", text="Location")
255 # layout.prop(landmark_selected,
256 # "base_pose_angle", text="Angle")
259 class VIEW3D_PT_vr_session_view(bpy.types.Panel):
260 bl_space_type = 'VIEW_3D'
261 bl_region_type = 'UI'
262 bl_category = "VR"
263 bl_label = "View"
265 def draw(self, context):
266 layout = self.layout
267 session_settings = context.window_manager.xr_session_settings
269 layout.use_property_split = True
270 layout.use_property_decorate = False # No animation.
272 layout.prop(session_settings, "show_floor", text="Floor")
273 layout.prop(session_settings, "show_annotation", text="Annotations")
275 layout.separator()
277 col = layout.column(align=True)
278 col.prop(session_settings, "clip_start", text="Clip Start")
279 col.prop(session_settings, "clip_end", text="End")
282 class VIEW3D_PT_vr_session(bpy.types.Panel):
283 bl_space_type = 'VIEW_3D'
284 bl_region_type = 'UI'
285 bl_category = "VR"
286 bl_label = "VR Session"
288 def draw(self, context):
289 layout = self.layout
290 session_settings = context.window_manager.xr_session_settings
292 layout.use_property_split = True
293 layout.use_property_decorate = False # No animation.
295 is_session_running = bpy.types.XrSessionState.is_running(context)
297 # Using SNAP_FACE because it looks like a stop icon -- I shouldn't
298 # have commit rights...
299 toggle_info = (
300 ("Start VR Session", 'PLAY') if not is_session_running else (
301 "Stop VR Session", 'SNAP_FACE')
303 layout.operator("wm.xr_session_toggle",
304 text=toggle_info[0], icon=toggle_info[1])
306 layout.separator()
308 layout.prop(session_settings, "use_positional_tracking")
311 class VIEW3D_OT_vr_landmark_add(bpy.types.Operator):
312 bl_idname = "view3d.vr_landmark_add"
313 bl_label = "Add VR Landmark"
314 bl_description = "Add a new VR landmark to the list and select it"
315 bl_options = {'UNDO', 'REGISTER'}
317 def execute(self, context):
318 scene = context.scene
319 landmarks = scene.vr_landmarks
321 landmarks.add()
323 # select newly created set
324 scene.vr_landmarks_selected = len(landmarks) - 1
326 return {'FINISHED'}
329 class VIEW3D_OT_vr_landmark_remove(bpy.types.Operator):
330 bl_idname = "view3d.vr_landmark_remove"
331 bl_label = "Remove VR Landmark"
332 bl_description = "Delete the selected VR landmark from the list"
333 bl_options = {'UNDO', 'REGISTER'}
335 def execute(self, context):
336 scene = context.scene
337 landmarks = scene.vr_landmarks
339 if len(landmarks) > 1:
340 landmark_selected_idx = scene.vr_landmarks_selected
341 landmarks.remove(landmark_selected_idx)
343 scene.vr_landmarks_selected -= 1
345 return {'FINISHED'}
348 class VIEW3D_OT_vr_landmark_activate(bpy.types.Operator):
349 bl_idname = "view3d.vr_landmark_activate"
350 bl_label = "Activate VR Landmark"
351 bl_description = "Change to the selected VR landmark from the list"
352 bl_options = {'UNDO', 'REGISTER'}
354 index: IntProperty(
355 name="Index",
356 options={'HIDDEN'},
359 def execute(self, context):
360 scene = context.scene
362 if self.index >= len(scene.vr_landmarks):
363 return {'CANCELLED'}
365 scene.vr_landmarks_active = (
366 self.index if self.properties.is_property_set(
367 "index") else scene.vr_landmarks_selected
370 return {'FINISHED'}
373 class VIEW3D_PT_vr_viewport_feedback(bpy.types.Panel):
374 bl_space_type = 'VIEW_3D'
375 bl_region_type = 'UI'
376 bl_category = "VR"
377 bl_label = "Viewport Feedback"
378 bl_options = {'DEFAULT_CLOSED'}
380 def draw(self, context):
381 layout = self.layout
382 view3d = context.space_data
384 layout.prop(view3d.shading, "vr_show_virtual_camera")
385 layout.prop(view3d, "mirror_xr_session")
388 class VIEW3D_GT_vr_camera_cone(Gizmo):
389 bl_idname = "VIEW_3D_GT_vr_camera_cone"
391 aspect = 1.0, 1.0
393 def draw(self, context):
394 import bgl
396 if not hasattr(self, "frame_shape"):
397 aspect = self.aspect
399 frame_shape_verts = (
400 (-aspect[0], -aspect[1], -1.0),
401 (aspect[0], -aspect[1], -1.0),
402 (aspect[0], aspect[1], -1.0),
403 (-aspect[0], aspect[1], -1.0),
405 lines_shape_verts = (
406 (0.0, 0.0, 0.0),
407 frame_shape_verts[0],
408 (0.0, 0.0, 0.0),
409 frame_shape_verts[1],
410 (0.0, 0.0, 0.0),
411 frame_shape_verts[2],
412 (0.0, 0.0, 0.0),
413 frame_shape_verts[3],
416 self.frame_shape = self.new_custom_shape(
417 'LINE_LOOP', frame_shape_verts)
418 self.lines_shape = self.new_custom_shape(
419 'LINES', lines_shape_verts)
421 # Ensure correct GL state (otherwise other gizmos might mess that up)
422 bgl.glLineWidth(1)
423 bgl.glEnable(bgl.GL_BLEND)
425 self.draw_custom_shape(self.frame_shape)
426 self.draw_custom_shape(self.lines_shape)
429 class VIEW3D_GGT_vr_viewer_pose(GizmoGroup):
430 bl_idname = "VIEW3D_GGT_vr_viewer_pose"
431 bl_label = "VR Viewer Pose Indicator"
432 bl_space_type = 'VIEW_3D'
433 bl_region_type = 'WINDOW'
434 bl_options = {'3D', 'PERSISTENT', 'SCALE', 'VR_REDRAWS'}
436 @classmethod
437 def poll(cls, context):
438 view3d = context.space_data
439 return (
440 view3d.shading.vr_show_virtual_camera and
441 bpy.types.XrSessionState.is_running(context) and
442 not view3d.mirror_xr_session
445 @staticmethod
446 def _get_viewer_pose_matrix(context):
447 from mathutils import Matrix, Quaternion
449 wm = context.window_manager
451 loc = wm.xr_session_state.viewer_pose_location
452 rot = wm.xr_session_state.viewer_pose_rotation
454 rotmat = Matrix.Identity(3)
455 rotmat.rotate(rot)
456 rotmat.resize_4x4()
457 transmat = Matrix.Translation(loc)
459 return transmat @ rotmat
461 def setup(self, context):
462 gizmo = self.gizmos.new(VIEW3D_GT_vr_camera_cone.bl_idname)
463 gizmo.aspect = 1 / 3, 1 / 4
465 gizmo.color = gizmo.color_highlight = 0.2, 0.6, 1.0
466 gizmo.alpha = 1.0
468 self.gizmo = gizmo
470 def draw_prepare(self, context):
471 self.gizmo.matrix_basis = self._get_viewer_pose_matrix(context)
474 classes = (
475 VIEW3D_PT_vr_session,
476 VIEW3D_PT_vr_session_view,
477 VIEW3D_PT_vr_landmarks,
478 VIEW3D_PT_vr_viewport_feedback,
480 VRLandmark,
481 VIEW3D_UL_vr_landmarks,
483 VIEW3D_OT_vr_landmark_add,
484 VIEW3D_OT_vr_landmark_remove,
485 VIEW3D_OT_vr_landmark_activate,
487 VIEW3D_GT_vr_camera_cone,
488 VIEW3D_GGT_vr_viewer_pose,
492 def register():
493 if not bpy.app.build_options.xr_openxr:
494 return
496 for cls in classes:
497 bpy.utils.register_class(cls)
499 bpy.types.Scene.vr_landmarks = CollectionProperty(
500 name="Landmark",
501 type=VRLandmark,
503 bpy.types.Scene.vr_landmarks_selected = IntProperty(
504 name="Selected Landmark"
506 bpy.types.Scene.vr_landmarks_active = IntProperty(
507 update=xr_landmark_active_update,
509 # View3DShading is the only per 3D-View struct with custom property
510 # support, so "abusing" that to get a per 3D-View option.
511 bpy.types.View3DShading.vr_show_virtual_camera = BoolProperty(
512 name="Show VR Camera"
515 bpy.app.handlers.load_post.append(ensure_default_vr_landmark)
518 def unregister():
519 if not bpy.app.build_options.xr_openxr:
520 return
522 for cls in classes:
523 bpy.utils.unregister_class(cls)
525 del bpy.types.Scene.vr_landmarks
526 del bpy.types.Scene.vr_landmarks_selected
527 del bpy.types.Scene.vr_landmarks_active
528 del bpy.types.View3DShading.vr_show_virtual_camera
530 bpy.app.handlers.load_post.remove(ensure_default_vr_landmark)
533 if __name__ == "__main__":
534 register()