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 #####
22 from bpy
.types
import (
26 from bpy
.props
import (
31 from bpy
.app
.handlers
import persistent
34 "name": "VR Scene Inspection",
35 "author": "Julian Eisel (Severin)",
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",
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
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
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
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
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
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(
150 type: bpy
.props
.EnumProperty(
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 "
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(
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",
183 update
=xr_landmark_base_pose_angle_update
,
187 def get_selected_landmark(context
):
188 scene
= context
.scene
189 landmarks
= scene
.vr_landmarks
192 None if (len(landmarks
) <
193 1) else landmarks
[scene
.vr_landmarks_selected
]
197 def get_active_landmark(context
):
198 scene
= context
.scene
199 landmarks
= scene
.vr_landmarks
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
):
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
)
223 class VIEW3D_PT_vr_landmarks(bpy
.types
.Panel
):
224 bl_space_type
= 'VIEW_3D'
225 bl_region_type
= 'UI'
227 bl_label
= "Landmarks"
228 bl_options
= {'DEFAULT_CLOSED'}
230 def draw(self
, context
):
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.
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'
265 def draw(self
, context
):
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")
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'
286 bl_label
= "VR Session"
288 def draw(self
, context
):
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...
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])
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
323 # select newly created set
324 scene
.vr_landmarks_selected
= len(landmarks
) - 1
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
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'}
359 def execute(self
, context
):
360 scene
= context
.scene
362 if self
.index
>= len(scene
.vr_landmarks
):
365 scene
.vr_landmarks_active
= (
366 self
.index
if self
.properties
.is_property_set(
367 "index") else scene
.vr_landmarks_selected
373 class VIEW3D_PT_vr_viewport_feedback(bpy
.types
.Panel
):
374 bl_space_type
= 'VIEW_3D'
375 bl_region_type
= 'UI'
377 bl_label
= "Viewport Feedback"
378 bl_options
= {'DEFAULT_CLOSED'}
380 def draw(self
, context
):
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"
393 def draw(self
, context
):
396 if not hasattr(self
, "frame_shape"):
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
= (
407 frame_shape_verts
[0],
409 frame_shape_verts
[1],
411 frame_shape_verts
[2],
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)
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'}
437 def poll(cls
, context
):
438 view3d
= context
.space_data
440 view3d
.shading
.vr_show_virtual_camera
and
441 bpy
.types
.XrSessionState
.is_running(context
) and
442 not view3d
.mirror_xr_session
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)
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
470 def draw_prepare(self
, context
):
471 self
.gizmo
.matrix_basis
= self
._get
_viewer
_pose
_matrix
(context
)
475 VIEW3D_PT_vr_session
,
476 VIEW3D_PT_vr_session_view
,
477 VIEW3D_PT_vr_landmarks
,
478 VIEW3D_PT_vr_viewport_feedback
,
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
,
493 if not bpy
.app
.build_options
.xr_openxr
:
497 bpy
.utils
.register_class(cls
)
499 bpy
.types
.Scene
.vr_landmarks
= CollectionProperty(
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
)
519 if not bpy
.app
.build_options
.xr_openxr
:
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__":