├── .gitignore ├── LICENSE.txt ├── README.md └── virtucamera_blender ├── __init__.py └── virtucamera_blender.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | VirtuCameraBlender 2 | 3 | Copyright (c) 2021 Pablo Javier Garcia Gonzalez. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 19 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 22 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 24 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VirtuCameraBlender 2 | ### Blender add-on to use with VirtuCamera iOS App for realtime camera motion capture. 3 | 4 | Follow the installation instructions from https://virtucamera.com/installation-in-blender/ 5 | -------------------------------------------------------------------------------- /virtucamera_blender/__init__.py: -------------------------------------------------------------------------------- 1 | # VirtuCameraBlender 2 | # Copyright (c) 2021-2022 Pablo Javier Garcia Gonzalez. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 18 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 19 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | bl_info = { 26 | "name" : "VirtuCamera", 27 | "author" : "Pablo Garcia", 28 | "description" : "Live camera motion capture", 29 | "blender" : (2, 80, 0), 30 | "version" : (1, 1, 1), 31 | "location" : "3D View > VirtuCamera", 32 | "warning" : "", 33 | "category" : "3D View" 34 | } 35 | 36 | import bpy 37 | from .virtucamera_blender import * 38 | 39 | classes = ( 40 | VirtuCameraState, 41 | VIEW3D_OT_virtucamera_start, 42 | VIEW3D_OT_virtucamera_stop, 43 | VIEW3D_OT_virtucamera_redraw, 44 | GRAPH_OT_virtucamera_euler_filter, 45 | VIEW3D_PT_virtucamera_main, 46 | ) 47 | 48 | def register(): 49 | for cls in classes: 50 | bpy.utils.register_class(cls) 51 | bpy.types.Scene.virtucamera = bpy.props.PointerProperty(type=VirtuCameraState) 52 | 53 | def unregister(): 54 | bpy.context.scene.virtucamera.server.stop_serving() 55 | for cls in reversed(classes): 56 | bpy.utils.unregister_class(cls) 57 | del bpy.types.Scene.virtucamera -------------------------------------------------------------------------------- /virtucamera_blender/virtucamera_blender.py: -------------------------------------------------------------------------------- 1 | # VirtuCameraBlender 2 | # Copyright (c) 2021-2022 Pablo Javier Garcia Gonzalez. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 18 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 19 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | # Python modules 26 | import os 27 | import sys 28 | import math 29 | import ctypes 30 | import traceback 31 | 32 | # Blender modules 33 | import bpy 34 | import bpy.utils.previews 35 | import gpu 36 | import mathutils 37 | 38 | # VirtuCamera API 39 | from .virtucamera import VCBase, VCServer 40 | 41 | plugin_version = (1, 2, 0) 42 | 43 | class VirtuCameraBlender(VCBase): 44 | # Constants 45 | TRANSFORM_CHANNELS = ("location", "rotation_euler", "rotation_quaternion", "rotation_axis_angle") 46 | B_TO_V_ROTATION_MAT = mathutils.Matrix(( 47 | (1, 0, 0, 0), 48 | (0, 0,-1, 0), 49 | (0, 1, 0, 0), 50 | (0, 0, 0, 1) 51 | )) 52 | V_TO_B_ROTATION_MAT = mathutils.Matrix(( 53 | (1, 0, 0, 0), 54 | (0, 0, 1, 0), 55 | (0,-1, 0, 0), 56 | (0, 0, 0, 1) 57 | )) 58 | 59 | # Cached Viewport capture rect 60 | last_rect_data = None 61 | 62 | # -- Utility Functions ------------------------------------ 63 | 64 | def camera_rect_changed(self, offset_value_x, offset_value_y, zoom_value, region_rect, camera_aspect_ratio): 65 | rect_data = (offset_value_x, offset_value_y, zoom_value, region_rect, camera_aspect_ratio) 66 | if self.last_rect_data != rect_data: 67 | self.last_rect_data = rect_data 68 | return True 69 | return False 70 | 71 | def view_zoom_factor(self, zoom_value): 72 | return ((zoom_value / 50 + math.sqrt(2)) / 2) ** 2 73 | 74 | def view_region_width_zoom_factor(self, zoom_factor, region_aspect_ratio, camera_aspect_ratio): 75 | return zoom_factor * min(camera_aspect_ratio, 1) / min(region_aspect_ratio, 1) 76 | 77 | def view_offset_factor(self, offset_value, zoom_factor): 78 | return offset_value * zoom_factor * -2 79 | 80 | def get_view_camera_rect(self): 81 | scene = bpy.context.scene 82 | render = scene.render 83 | context = scene.virtucamera.contexts['start'] 84 | region = context['region'] 85 | r3d = context['space_data'].region_3d 86 | 87 | zoom_value = r3d.view_camera_zoom 88 | offset_value_x = r3d.view_camera_offset[0] 89 | offset_value_y = r3d.view_camera_offset[1] 90 | region_rect = (region.x, region.y, region.width, region.height) 91 | camera_aspect_ratio = (render.resolution_x * render.pixel_aspect_x) / (render.resolution_y * render.pixel_aspect_y) 92 | 93 | # Check if zoom, offset, aspect or region size/pos changes and return cached rect otherwise 94 | if self.camera_rect_changed(offset_value_x, offset_value_y, zoom_value, region_rect, camera_aspect_ratio): 95 | region_aspect_ratio = region.width / region.height 96 | 97 | zoom_factor = self.view_zoom_factor(zoom_value) 98 | 99 | width_factor = self.view_region_width_zoom_factor(zoom_factor, region_aspect_ratio, camera_aspect_ratio) 100 | width = int(region.width * width_factor) 101 | height = int(width / camera_aspect_ratio) 102 | 103 | offset_factor_x = self.view_offset_factor(offset_value_x, zoom_factor) 104 | offset_factor_y = self.view_offset_factor(offset_value_y, zoom_factor) 105 | x = int(region.x + (region.width - width) * 0.5 + region.width * offset_factor_x) 106 | y = int(region.y + (region.height - height) * 0.5 + region.height * offset_factor_y) 107 | 108 | # Clamp values to region rect 109 | width = min(width, region.width) 110 | height = min(height, region.height) 111 | x = min(max(x, region.x), region.x + region.width - width) 112 | y = min(max(y, region.y), region.y + region.height - height) 113 | 114 | self.last_rect = (x, y, width, height) 115 | 116 | return self.last_rect 117 | 118 | def init_capture_buffer(self, width, height): 119 | self.buffer = gpu.types.Buffer('UBYTE', [4, width * height]) 120 | """ 121 | Based on the C Struct defined in gpu_py_buffer.h for the gpu.types.Buffer object, extract the buffer pointer 122 | 123 | typedef struct BPyGPUBuffer { 124 | PyObject_VAR_HEAD 125 | PyObject *parent; 126 | 127 | int format; 128 | int shape_len; 129 | Py_ssize_t *shape; 130 | 131 | union { 132 | char *as_byte; 133 | int *as_int; 134 | uint *as_uint; 135 | float *as_float; 136 | 137 | void *as_void; 138 | } buf; 139 | } BPyGPUBuffer; 140 | """ 141 | # Get the size of the Buffer Python object 142 | buffer_obj_size = sys.getsizeof(self.buffer) 143 | # Get the size of void * C-type 144 | buffer_pointer_size = ctypes.sizeof(ctypes.c_void_p) 145 | # Calculate the address to the pointer assuming that it's at the end of the C Struct 146 | buffer_pointer_addr = id(self.buffer) + buffer_obj_size - buffer_pointer_size 147 | # Get the actual pointer value as a Python Int 148 | self.buffer_pointer = (ctypes.c_void_p).from_address(buffer_pointer_addr).value 149 | 150 | 151 | def get_script_files(self): 152 | scripts_dir = bpy.context.scene.virtucamera.custom_scripts_dir 153 | if not os.path.isdir(scripts_dir): 154 | return [] 155 | dir_files = os.listdir(scripts_dir) 156 | dir_files.sort() 157 | 158 | valid_files = [] 159 | for file in dir_files: 160 | if file.endswith(".py"): 161 | filepath = os.path.join(scripts_dir, file) 162 | if os.path.isdir(filepath): 163 | continue 164 | valid_files.append(filepath) 165 | 166 | return valid_files 167 | 168 | 169 | # SCENE STATE RELATED METHODS: 170 | # --------------------------- 171 | 172 | def get_playback_state(self, vcserver): 173 | """ Must Return the playback state of the scene as a tuple or list 174 | in the following order: (current_frame, range_start, range_end) 175 | * current_frame (float) - The current frame number. 176 | * range_start (float) - Animation range start frame number. 177 | * range_end (float) - Animation range end frame number. 178 | 179 | Parameters 180 | ---------- 181 | vcserver : virtucamera.VCServer object 182 | Instance of virtucamera.VCServer calling this method. 183 | 184 | Returns 185 | ------- 186 | tuple or list of 3 floats 187 | playback state as (current_frame, range_start, range_end) 188 | """ 189 | 190 | current_frame = bpy.context.scene.frame_current 191 | range_start = bpy.context.scene.frame_start 192 | range_end = bpy.context.scene.frame_end 193 | return (current_frame, range_start, range_end) 194 | 195 | 196 | def get_playback_fps(self, vcserver): 197 | """ Must return a float value with the scene playback rate 198 | in Frames Per Second. 199 | 200 | Parameters 201 | ---------- 202 | vcserver : virtucamera.VCServer object 203 | Instance of virtucamera.VCServer calling this method. 204 | 205 | Returns 206 | ------- 207 | float 208 | scene playback rate in FPS. 209 | """ 210 | 211 | return bpy.context.scene.render.fps 212 | 213 | 214 | def set_frame(self, vcserver, frame): 215 | """ Must set the current frame number on the scene 216 | 217 | Parameters 218 | ---------- 219 | vcserver : virtucamera.VCServer object 220 | Instance of virtucamera.VCServer calling this method. 221 | frame : float 222 | The current frame number. 223 | """ 224 | 225 | bpy.context.scene.frame_current = int(frame) 226 | 227 | 228 | def set_playback_range(self, vcserver, start, end): 229 | """ Must set the animation frame range on the scene 230 | 231 | Parameters 232 | ---------- 233 | vcserver : virtucamera.VCServer object 234 | Instance of virtucamera.VCServer calling this method. 235 | start : float 236 | Animation range start frame number. 237 | end : float 238 | Animation range end frame number. 239 | """ 240 | 241 | bpy.context.scene.frame_start = int(start) 242 | bpy.context.scene.frame_end = int(end) 243 | 244 | 245 | def start_playback(self, vcserver, forward): 246 | """ This method must start the playback of animation in the scene. 247 | Not used at the moment, but must be implemented just in case 248 | the app starts using it in the future. At the moment 249 | VCBase.set_frame() is called instead. 250 | 251 | Parameters 252 | ---------- 253 | vcserver : virtucamera.VCServer object 254 | Instance of virtucamera.VCServer calling this method. 255 | forward : bool 256 | if True, play the animation forward, if False, play it backwards. 257 | """ 258 | 259 | if not bpy.context.screen.is_animation_playing: 260 | # This operator acts like a toggle, so we need to first check if it's playing 261 | bpy.ops.screen.animation_play(reverse=(not forward), sync=True) 262 | 263 | 264 | def stop_playback(self, vcserver): 265 | """ This method must stop the playback of animation in the scene. 266 | Not used at the moment, but must be implemented just in case 267 | the app starts using it in the future. 268 | 269 | Parameters 270 | ---------- 271 | vcserver : virtucamera.VCServer object 272 | Instance of virtucamera.VCServer calling this method. 273 | """ 274 | 275 | bpy.ops.screen.animation_cancel(restore_frame=False) 276 | 277 | 278 | # CAMERA RELATED METHODS: 279 | # ----------------------- 280 | 281 | def get_scene_cameras(self, vcserver): 282 | """ Must Return a list or tuple with the names of all the scene cameras. 283 | 284 | Parameters 285 | ---------- 286 | vcserver : virtucamera.VCServer object 287 | Instance of virtucamera.VCServer calling this method. 288 | 289 | Returns 290 | ------- 291 | tuple or list 292 | names of all the scene cameras. 293 | """ 294 | 295 | scene_cameras = [] 296 | for obj in bpy.data.objects: 297 | # Filter invisible cameras, as euler filter only works on visible cameras. 298 | if obj.type == "CAMERA" and obj.visible_get(): 299 | scene_cameras.append(obj) 300 | camera_names = [camera.name for camera in scene_cameras] 301 | return camera_names 302 | 303 | 304 | def get_camera_exists(self, vcserver, camera_name): 305 | """ Must Return True if the specified camera exists in the scene, 306 | False otherwise. 307 | 308 | Parameters 309 | ---------- 310 | vcserver : virtucamera.VCServer object 311 | Instance of virtucamera.VCServer calling this method. 312 | camera_name : str 313 | Name of the camera to check for. 314 | 315 | Returns 316 | ------- 317 | bool 318 | 'True' if the camera 'camera_name' exists, 'False' otherwise. 319 | """ 320 | 321 | # Filter invisible cameras, as euler filter only works on visible cameras. 322 | if camera_name in bpy.data.objects and bpy.data.objects[camera_name].visible_get(): 323 | return True 324 | return False 325 | 326 | 327 | def get_camera_has_keys(self, vcserver, camera_name): 328 | """ Must Return whether the specified camera has animation keyframes 329 | in the transform or flocal length parameters, as a tuple or list, 330 | in the following order: (transform_has_keys, focal_length_has_keys) 331 | * transform_has_keys (bool) - True if the transform has keyframes. 332 | * focal_length_has_keys (bool) - True if the flen has keyframes. 333 | 334 | Parameters 335 | ---------- 336 | vcserver : virtucamera.VCServer object 337 | Instance of virtucamera.VCServer calling this method. 338 | camera_name : str 339 | Name of the camera to check for. 340 | 341 | Returns 342 | ------- 343 | tuple or list of 2 bool 344 | whether the camera 'camera_name' has keys or not as 345 | (transform_has_keys, focal_length_has_keys) 346 | """ 347 | 348 | camera = bpy.data.objects[camera_name] 349 | 350 | transform_has_keys = False 351 | if camera.animation_data and camera.animation_data.action: 352 | for fcu in camera.animation_data.action.fcurves: 353 | if fcu.data_path in self.TRANSFORM_CHANNELS: 354 | transform_has_keys = True 355 | break 356 | 357 | focal_length_has_keys = False 358 | if camera.data.animation_data and camera.data.animation_data.action: 359 | for fcu in camera.data.animation_data.action.fcurves: 360 | if fcu.data_path == "lens": 361 | focal_length_has_keys = True 362 | break 363 | 364 | return (transform_has_keys, focal_length_has_keys) 365 | 366 | 367 | def get_camera_focal_length(self, vcserver, camera_name): 368 | """ Must Return the focal length value of the specified camera. 369 | 370 | Parameters 371 | ---------- 372 | vcserver : virtucamera.VCServer object 373 | Instance of virtucamera.VCServer calling this method. 374 | camera_name : str 375 | Name of the camera to get the data from. 376 | 377 | Returns 378 | ------- 379 | float 380 | focal length value of the camera 'camera_name'. 381 | """ 382 | 383 | camera_data = bpy.data.objects[camera_name].data 384 | return camera_data.lens 385 | 386 | 387 | def get_camera_transform(self, vcserver, camera_name): 388 | """ Must return a tuple or list of 16 floats with the 4x4 389 | transform matrix of the specified camera. 390 | 391 | * The up axis must be Y+ 392 | * The order must be: 393 | (rxx, rxy, rxz, 0, 394 | ryx, ryy, ryz, 0, 395 | rzx, rzy, rzz, 0, 396 | tx, ty, tz, 1) 397 | Being 'r' rotation and 't' translation, 398 | 399 | Is your responsability to rotate or transpose the matrix if needed, 400 | most 3D softwares offer fast APIs to do so. 401 | 402 | Parameters 403 | ---------- 404 | vcserver : virtucamera.VCServer object 405 | Instance of virtucamera.VCServer calling this method. 406 | camera_name : str 407 | Name of the camera to get the data from. 408 | 409 | Returns 410 | ------- 411 | tuple or list of 16 float 412 | 4x4 transform matrix as 413 | (rxx, rxy, rxz, 0, ryx, ryy, ryz, 0, rzx, rzy, rzz, 0 , tx, ty, tz, 1) 414 | """ 415 | 416 | camera_matrix = bpy.data.objects[camera_name].matrix_local.transposed() 417 | # Blender is Z+ up, so we rotate the transform matrix 418 | camera_matrix @= self.B_TO_V_ROTATION_MAT 419 | camera_matrix_tuple = ( 420 | *camera_matrix[0], 421 | *camera_matrix[1], 422 | *camera_matrix[2], 423 | *camera_matrix[3] 424 | ) 425 | return camera_matrix_tuple 426 | 427 | 428 | def set_camera_focal_length(self, vcserver, camera_name, focal_length): 429 | """ Must set the focal length of the specified camera. 430 | 431 | Parameters 432 | ---------- 433 | vcserver : virtucamera.VCServer object 434 | Instance of virtucamera.VCServer calling this method. 435 | camera_name : str 436 | Name of the camera to set the focal length to. 437 | focal_length : float 438 | focal length value to be set on the camera 'camera_name' 439 | """ 440 | 441 | camera_data = bpy.data.objects[camera_name].data 442 | camera_data.lens = focal_length 443 | 444 | 445 | def set_camera_transform(self, vcserver, camera_name, transform_matrix): 446 | """ Must set the transform of the specified camera. 447 | The transform matrix is provided as a tuple of 16 floats 448 | with a 4x4 transform matrix. 449 | 450 | * The up axis is Y+ 451 | * The order is: 452 | (rxx, rxy, rxz, 0, 453 | ryx, ryy, ryz, 0, 454 | rzx, rzy, rzz, 0, 455 | tx, ty, tz, 1) 456 | Being 'r' rotation and 't' translation, 457 | 458 | Is your responsability to rotate or transpose the matrix if needed, 459 | most 3D softwares offer fast APIs to do so. 460 | 461 | Parameters 462 | ---------- 463 | vcserver : virtucamera.VCServer object 464 | Instance of virtucamera.VCServer calling this method. 465 | camera_name : str 466 | Name of the camera to set the transform to. 467 | transform_matrix : tuple of 16 floats 468 | transformation matrix to be set on the camera 'camera_name' 469 | """ 470 | 471 | camera = bpy.data.objects[camera_name] 472 | matrix = mathutils.Matrix(( 473 | transform_matrix[0:4], 474 | transform_matrix[4:8], 475 | transform_matrix[8:12], 476 | transform_matrix[12:16] 477 | )) 478 | # Blender is Z+ up, so we rotate the transform matrix 479 | matrix @= self.V_TO_B_ROTATION_MAT 480 | matrix.transpose() 481 | camera.matrix_local = matrix 482 | 483 | 484 | def set_camera_flen_keys(self, vcserver, camera_name, keyframes, focal_length_values): 485 | """ Must set keyframes on the focal length of the specified camera. 486 | The frame numbers are provided as a tuple of floats and 487 | the focal length values are provided as a tuple of floats 488 | with a focal length value for every keyframe. 489 | 490 | The first element of the 'keyframes' tuple corresponds to the first 491 | element of the 'focal_length_values' tuple, the second to the second, 492 | and so on. 493 | 494 | Parameters 495 | ---------- 496 | vcserver : virtucamera.VCServer object 497 | Instance of virtucamera.VCServer calling this method. 498 | camera_name : str 499 | Name of the camera to set the keyframes to. 500 | keyframes : tuple of floats 501 | Frame numbers to create the keyframes on. 502 | focal_length_values : tuple of floats 503 | focal length values to be set as keyframes on the camera 'camera_name' 504 | """ 505 | 506 | camera_data = bpy.data.objects[camera_name].data 507 | for keyframe, focal_length in zip(keyframes, focal_length_values): 508 | camera_data.lens = focal_length 509 | camera_data.keyframe_insert('lens', frame=keyframe) 510 | 511 | 512 | def set_camera_transform_keys(self, vcserver, camera_name, keyframes, transform_matrix_values): 513 | """ Must set keyframes on the transform of the specified camera. 514 | The frame numbers are provided as a tuple of floats and 515 | the transform matrixes are provided as a tuple of tuples of 16 floats 516 | with 4x4 transform matrixes, with a matrix for every keyframe. 517 | 518 | The first element of the 'keyframes' tuple corresponds to the first 519 | element of the 'transform_matrix_values' tuple, the second to the second, 520 | and so on. 521 | 522 | * The up axis is Y+ 523 | * The order is: 524 | (rxx, rxy, rxz, 0, 525 | ryx, ryy, ryz, 0, 526 | rzx, rzy, rzz, 0, 527 | tx, ty, tz, 1) 528 | Being 'r' rotation and 't' translation, 529 | 530 | Is your responsability to rotate or transpose the matrixes if needed, 531 | most 3D softwares offer fast APIs to do so. 532 | 533 | Parameters 534 | ---------- 535 | vcserver : virtucamera.VCServer object 536 | Instance of virtucamera.VCServer calling this method. 537 | camera_name : str 538 | Name of the camera to set the keyframes to. 539 | keyframes : tuple of floats 540 | Frame numbers to create the keyframes on. 541 | transform_matrix_values : tuple of tuples of 16 floats 542 | transformation matrixes to be set as keyframes on the camera 'camera_name' 543 | """ 544 | 545 | camera = bpy.data.objects[camera_name] 546 | for keyframe, matrix in zip(keyframes, transform_matrix_values): 547 | self.set_camera_transform(vcserver, camera_name, matrix) 548 | camera.keyframe_insert('location', frame=keyframe) 549 | camera.keyframe_insert('rotation_euler', frame=keyframe) 550 | bpy.ops.graph.virtucamera_euler_filter(object_name=camera_name) 551 | 552 | 553 | def remove_camera_keys(self, vcserver, camera_name): 554 | """ This method must remove all transform 555 | and focal length keyframes in the specified camera. 556 | 557 | Parameters 558 | ---------- 559 | vcserver : virtucamera.VCServer object 560 | Instance of virtucamera.VCServer calling this method. 561 | camera_name : str 562 | Name of the camera to remove the keyframes from. 563 | """ 564 | 565 | camera = bpy.data.objects[camera_name] 566 | if camera.animation_data and camera.animation_data.action: 567 | for fcu in camera.animation_data.action.fcurves: 568 | if fcu.data_path in self.TRANSFORM_CHANNELS: 569 | camera.animation_data.action.fcurves.remove(fcu) 570 | 571 | if camera.data.animation_data and camera.data.animation_data.action: 572 | for fcu in camera.data.animation_data.action.fcurves: 573 | if fcu.data_path == "lens": 574 | camera.data.animation_data.action.fcurves.remove(fcu) 575 | break 576 | 577 | 578 | def create_new_camera(self, vcserver): 579 | """ This method must create a new camera in the scene 580 | and return its name. 581 | 582 | Parameters 583 | ---------- 584 | vcserver : virtucamera.VCServer object 585 | Instance of virtucamera.VCServer calling this method. 586 | 587 | Returns 588 | ------- 589 | str 590 | Newly created camera name. 591 | """ 592 | 593 | bpy.ops.object.camera_add(enter_editmode=False) 594 | return bpy.context.scene.objects[-1].name 595 | 596 | 597 | # VIEWPORT CAPTURE RELATED METHODS: 598 | # --------------------------------- 599 | 600 | def capture_will_start(self, vcserver): 601 | """ This method is called whenever a client app requests a video 602 | feed from the viewport. Usefull to init a pixel buffer 603 | or other objects you may need to capture the viewport 604 | 605 | IMPORTANT! Calling vcserver.set_capture_resolution() and 606 | vcserver.set_capture_mode() here is a must. Please check 607 | the documentation for those methods. 608 | 609 | You can also call vcserver.set_vertical_flip() here optionally, 610 | if you need to flip your pixel buffer. Disabled by default. 611 | 612 | Parameters 613 | ---------- 614 | vcserver : virtucamera.VCServer object 615 | Instance of virtucamera.VCServer calling this method. 616 | """ 617 | 618 | (x, y, width, height) = self.get_view_camera_rect() 619 | self.init_capture_buffer(width, height) 620 | vcserver.set_capture_resolution(width, height) 621 | vcserver.set_capture_mode(vcserver.CAPMODE_BUFFER_POINTER, vcserver.CAPFORMAT_UBYTE_RGBA) 622 | vcserver.set_vertical_flip(True) 623 | 624 | 625 | def capture_did_end(self, vcserver): 626 | """ Optional, this method is called whenever a client app 627 | stops the viewport video feed. Usefull to destroy a pixel buffer 628 | or other objects you may have created to capture the viewport. 629 | 630 | Parameters 631 | ---------- 632 | vcserver : virtucamera.VCServer object 633 | Instance of virtucamera.VCServer calling this method. 634 | """ 635 | 636 | del self.buffer 637 | 638 | 639 | def get_capture_pointer(self, vcserver, camera_name): 640 | """ If vcserver.capture_mode == vcserver.CAPMODE_BUFFER_POINTER, 641 | it must return an int representing a memory address to the first 642 | element of a contiguous buffer containing raw pixels of the 643 | viewport image. The buffer must be kept allocated untill the next 644 | call to this function, is your responsability to do so. 645 | If you don't use CAPMODE_BUFFER_POINTER 646 | you don't need to overload this method. 647 | 648 | If the capture resolution has changed in size from the previous call to 649 | this method, vcserver.set_capture_resolution() must be called here 650 | before returning. You can use vcserver.capture_width and 651 | vcserver.capture_height to check the previous resolution. 652 | 653 | The name of the camera selected in the app is provided, 654 | as can be usefull to set-up the viewport render in some cases. 655 | 656 | Parameters 657 | ---------- 658 | vcserver : virtucamera.VCServer object 659 | Instance of virtucamera.VCServer calling this method. 660 | camera_name : str 661 | Name of the camera that is currently selected in the App. 662 | 663 | Returns 664 | ------- 665 | int 666 | value of the memory address to the first element of the buffer. 667 | """ 668 | 669 | (x, y, width, height) = self.get_view_camera_rect() 670 | 671 | if width != vcserver.capture_width or height != vcserver.capture_height: 672 | vcserver.set_capture_resolution(width, height) 673 | self.init_capture_buffer(width, height) 674 | 675 | fb = gpu.state.active_framebuffer_get() 676 | with fb.bind(): 677 | fb.read_color(x, y, width, height, 4, 0, 'UBYTE', data=self.buffer) 678 | 679 | return self.buffer_pointer 680 | 681 | 682 | def look_through_camera(self, vcserver, camera_name): 683 | """ This method must set the viewport to look through 684 | the specified camera. 685 | 686 | Parameters 687 | ---------- 688 | vcserver : virtucamera.VCServer object 689 | Instance of virtucamera.VCServer calling this method. 690 | camera_name : str 691 | Name of the camera to look through 692 | """ 693 | 694 | camera = bpy.data.objects[camera_name] 695 | context = bpy.context.scene.virtucamera.contexts['start'] 696 | bpy.context.scene.camera = camera 697 | context['space_data'].region_3d.view_perspective = 'CAMERA' 698 | 699 | 700 | # APP/SERVER FEEDBACK METHODS: 701 | # --------------------------- 702 | 703 | def client_connected(self, vcserver, client_ip, client_port): 704 | """ Optional, this method is called whenever a client app 705 | connects to the server. Usefull to give the user 706 | feedback about a successfull connection. 707 | 708 | Parameters 709 | ---------- 710 | vcserver : virtucamera.VCServer object 711 | Instance of virtucamera.VCServer calling this method. 712 | client_ip : str 713 | ip address of the remote client 714 | client_port : int 715 | port number of the remote client 716 | """ 717 | 718 | bpy.ops.view3d.virtucamera_redraw() 719 | 720 | 721 | def client_disconnected(self, vcserver): 722 | """ Optional, this method is called whenever a client app 723 | disconnects from the server, even if it's disconnected by calling 724 | stop_serving() with the virtucamera.VCServer API. Usefull to give 725 | the user feedback about the disconnection. 726 | 727 | Parameters 728 | ---------- 729 | vcserver : virtucamera.VCServer object 730 | Instance of virtucamera.VCServer calling this method. 731 | """ 732 | 733 | bpy.ops.view3d.virtucamera_redraw() 734 | 735 | 736 | def current_camera_changed(self, vcserver, current_camera): 737 | """ Optional, this method is called when the user selects 738 | a different camera from the app. Usefull to give the user 739 | feedback about the currently selected camera. 740 | 741 | Parameters 742 | ---------- 743 | vcserver : virtucamera.VCServer object 744 | Instance of virtucamera.VCServer calling this method. 745 | current_camera : str 746 | Name of the new selected camera 747 | """ 748 | 749 | bpy.ops.view3d.virtucamera_redraw() 750 | 751 | 752 | def server_did_stop(self, vcserver): 753 | """ Optional, calling stop_serving() on virtucamera.VCServer 754 | doesn't instantly stop the server, it is done in the background 755 | due to the asyncronous nature of some of its processes. 756 | This method is called when all services have been completely 757 | stopped. 758 | 759 | Parameters 760 | ---------- 761 | vcserver : virtucamera.VCServer object 762 | Instance of virtucamera.VCServer calling this method. 763 | """ 764 | 765 | bpy.ops.view3d.virtucamera_redraw() 766 | 767 | 768 | # CUSTOM SCRIPT METHODS: 769 | # ---------------------- 770 | 771 | def get_script_labels(self, vcserver): 772 | """ Optionally Return a list or tuple of str with the labels of 773 | custom scripts to be called from VirtuCamera App. Each label is 774 | a string that identifies the script that will be showed 775 | as a button in the App. 776 | The order of the labels is important. Later if the App asks 777 | to execute a script, an index based on this order will be provided 778 | to VCBase.execute_script(), so that method must also be implemented. 779 | Parameters 780 | ---------- 781 | vcserver : virtucamera.VCServer object 782 | Instance of virtucamera.VCServer calling this method. 783 | Returns 784 | ------- 785 | tuple or list of str 786 | custom script labels. 787 | """ 788 | script_files = self.get_script_files() 789 | 790 | labels = [] 791 | for filepath in script_files: 792 | filename = os.path.split(filepath)[1] 793 | tokens = filename.split("_") 794 | if len(tokens) > 1 and tokens[0].isdigit(): 795 | prefix_len = len(tokens[0]) 796 | label = filename[prefix_len+1:-3] 797 | labels.append(label) 798 | else: 799 | label = filename[:-3] 800 | labels.append(label) 801 | 802 | return labels 803 | 804 | 805 | def execute_script(self, vcserver, script_index, current_camera): 806 | """ Only required if VCBase.get_script_labels() 807 | has been implemented. This method is called whenever the user 808 | taps on a custom script button in the app. 809 | 810 | Each of the labels returned from VCBase.get_script_labels() 811 | identify a custom script that is showed as a button in the app. 812 | The order of the labels is important and 'script_index' is a 0-based 813 | index representing what script to execute from that list/tuple. 814 | This function must return True if the script executed correctly, 815 | False if there where errors. It's recommended to print any errors, 816 | so that the user has some feedback about what went wrong. 817 | You may want to provide a way for the user to refer to the currently 818 | selected camera in their scripts, so that they can act over it. 819 | 'current_camera' is provided for this situation. 820 | Parameters 821 | ---------- 822 | vcserver : virtucamera.VCServer object 823 | Instance of virtucamera.VCServer calling this method. 824 | script_index : int 825 | Script number to be executed. 826 | current_camera : str 827 | Name of the currently selected camera 828 | """ 829 | script_files = self.get_script_files() 830 | 831 | if script_index >= len(script_files): 832 | print("Can't execute script "+str(script_index+1)+". Reason: Script doesn't exist") 833 | return False 834 | 835 | try: 836 | with open(script_files[script_index], "r") as script_file: 837 | script_code = script_file.read() 838 | except: 839 | traceback.print_exc() 840 | print("Can't execute script "+str(script_index+1)+". Reason: Unable to open file '"+script_files[script_index])+"'" 841 | return False 842 | 843 | if script_code == '': 844 | print("Can't execute script "+str(script_index+1)+". Reason: Empty script") 845 | return False 846 | 847 | selcam_var_def = 'vc_selcam = "'+current_camera+'"\n' 848 | script_code = selcam_var_def + script_code 849 | # use try to prevent any possible errors in the script from stopping plug-in execution 850 | try: 851 | exec(script_code) 852 | return True 853 | except: 854 | # Print traceback to inform the user 855 | traceback.print_exc() 856 | return False 857 | 858 | 859 | # Blender will call this function regularly to keep VCServer working in the background 860 | def timer_function(): 861 | vcserver = bpy.context.scene.virtucamera.server 862 | vcserver.execute_pending_events() 863 | if vcserver.is_event_loop_running: 864 | return 0.0 # Call again as soon as possible 865 | else: 866 | return None # Stop calling this function 867 | 868 | # Called whenever the scripts directory path changes 869 | def update_script_labels(self, context): 870 | vcserver = bpy.context.scene.virtucamera.server 871 | vcserver.update_script_labels() 872 | 873 | class VirtuCameraState(bpy.types.PropertyGroup): 874 | tcp_port: bpy.props.IntProperty( 875 | name = "Server TCP Port", 876 | description = "TCP port to listen for VirtuCamera App connections", 877 | default = 23354, 878 | min = 0, 879 | max = 65535 880 | ) 881 | custom_scripts_dir: bpy.props.StringProperty( 882 | name = "Scripts", 883 | description = "Path to directory containing custom Python scripts to be shown as buttons in the app.\nIf you prefix file names with a number, it will be used to order the buttons\n(e.g.: 1_myscript.py)", 884 | default = "", 885 | subtype = "DIR_PATH", 886 | update = update_script_labels 887 | ) 888 | server = VCServer( 889 | platform = "Blender", 890 | plugin_version = plugin_version, 891 | event_mode = VCServer.EVENTMODE_PULL, 892 | vcbase = VirtuCameraBlender() 893 | ) 894 | custom_icons = bpy.utils.previews.new() 895 | contexts = dict() 896 | 897 | class VIEW3D_OT_virtucamera_start(bpy.types.Operator): 898 | bl_idname = "view3d.virtucamera_start" 899 | bl_label = "Start Serving" 900 | bl_description ="Start listening for incoming connections, then you can scan the QR Code from the App" 901 | 902 | @classmethod 903 | def poll(cls, context): 904 | server = context.scene.virtucamera.server 905 | return not server.is_serving 906 | 907 | def execute(self, context): 908 | state = context.scene.virtucamera 909 | server = state.server 910 | server.start_serving(state.tcp_port) 911 | if not server.is_serving: 912 | return {'FINISHED'} 913 | bpy.app.timers.register(timer_function) 914 | file_path = os.path.join(os.path.dirname(__file__), 'virtucamera_qr_img.png') 915 | server.write_qr_image_png(file_path, 3) 916 | state.custom_icons.clear() 917 | state.custom_icons.load('qr_image', file_path, 'IMAGE') 918 | state.contexts['start'] = context.copy() 919 | return {'FINISHED'} 920 | 921 | class VIEW3D_OT_virtucamera_stop(bpy.types.Operator): 922 | bl_idname = "view3d.virtucamera_stop" 923 | bl_label = "Stop Serving" 924 | bl_description ="Stop listening for incoming connections from VirtuCamera App" 925 | 926 | @classmethod 927 | def poll(cls, context): 928 | server = context.scene.virtucamera.server 929 | return server.is_serving 930 | 931 | def execute(self, context): 932 | server = context.scene.virtucamera.server 933 | server.stop_serving() 934 | return {'FINISHED'} 935 | 936 | class VIEW3D_OT_virtucamera_redraw(bpy.types.Operator): 937 | bl_idname = "view3d.virtucamera_redraw" 938 | bl_label = "Redraw UI" 939 | 940 | def execute(self, context): 941 | for window in context.window_manager.windows: 942 | screen = window.screen 943 | for area in screen.areas: 944 | if area.type == 'VIEW_3D': 945 | area.tag_redraw() 946 | return {'FINISHED'} 947 | 948 | # Wrapper to apply Blender's Euler Filter on specified camera object from any context 949 | class GRAPH_OT_virtucamera_euler_filter(bpy.types.Operator): 950 | bl_idname = "graph.virtucamera_euler_filter" 951 | bl_label = "Euler Filter" 952 | 953 | object_name: bpy.props.StringProperty(name="Object Name") 954 | 955 | def execute(self, context): 956 | camera = bpy.data.objects[self.object_name] 957 | area = context.window_manager.windows[0].screen.areas[0] 958 | prev_cam_select = camera.select_get() 959 | prev_area_type = area.type 960 | try: 961 | camera.select_set(True) 962 | area.type = 'GRAPH_EDITOR' 963 | override = context.copy() 964 | override['area'] = area 965 | fcurves = [fcu for fcu in camera.animation_data.action.fcurves if fcu.data_path == 'rotation_euler'] 966 | override['selected_visible_fcurves'] = fcurves 967 | bpy.ops.graph.euler_filter(override) 968 | except: 969 | area.type = prev_area_type 970 | camera.select_set(prev_cam_select) 971 | raise 972 | area.type = prev_area_type 973 | camera.select_set(prev_cam_select) 974 | return {'FINISHED'} 975 | 976 | class VIEW3D_PT_virtucamera_main(bpy.types.Panel): 977 | bl_idname = "VIEW3D_PT_virtucamera_main" 978 | bl_label = 'VirtuCamera' 979 | bl_space_type = 'VIEW_3D' 980 | bl_region_type = 'UI' 981 | bl_category = 'VirtuCamera' 982 | 983 | def draw(self, context): 984 | state = context.scene.virtucamera 985 | server = state.server 986 | layout = self.layout 987 | column=layout.column() 988 | column.label(text='v%d.%d.%d (server v%d.%d.%d)' % (plugin_version + server.SERVER_VERSION)) 989 | row = layout.row() 990 | if server.is_serving: 991 | row.enabled = False 992 | row.prop(state, "tcp_port") 993 | layout.operator('view3d.virtucamera_start') 994 | layout.operator('view3d.virtucamera_stop') 995 | if server.is_serving and not server.is_connected and 'qr_image' in state.custom_icons: 996 | column=layout.column() 997 | column.label(text='Server Ready') 998 | column.label(text='Connect through the App') 999 | layout.template_icon(icon_value=state.custom_icons['qr_image'].icon_id, scale=6) 1000 | elif server.is_connected: 1001 | column=layout.column() 1002 | column.label(text='Connected: '+server.client_ip, icon='CHECKMARK') 1003 | if server.current_camera: 1004 | column.label(text=server.current_camera, icon='VIEW_CAMERA') 1005 | column = layout.column() 1006 | column.separator() 1007 | column.prop(state, "custom_scripts_dir") --------------------------------------------------------------------------------