├── README.md ├── cable_wizard.py ├── enable_gpus.py ├── filter_foreground.py ├── grouping_pies.py ├── marker_weight.py ├── masking_pies.py ├── movieclip_3d_reconstruction.py ├── nla_player.py ├── render_tweaker.py ├── setup_tracking_scene_new.py ├── snapping_pies.py ├── snapping_pies_2.79.py ├── tracking_pies.py ├── tracking_pies_280.py ├── tracking_tools.py ├── viewport_pies.py ├── vr_recorder.py └── vrais_tools.py /README.md: -------------------------------------------------------------------------------- 1 | # Blender Scripts 2 | In this repository we upload various scripts to improve our inhouse workflow with Blender. 3 | 4 | ## Render Tweaker 5 | The purpose of this script is to help you with the process of finding the best render settings by tweaking sample and bounces parameters of the Cycles render engine and comparing it to previous results using the render slots in the Image Editor. 6 | ### Usage 7 | There are two ways to use it. First you can store the current settings by pressing the Save Settings button in the Sampling Panel of the render properties. Now you can tweak all cycles render settings, rerender, compare and restore the previous settings by pressing the Restore Settings button. 8 | But you can also use the 8 render slots of the image editor for this. Enable the Record Render Settings button in the header of the Image Editor to save your settings of the current slot. As long as the button is enabled there will be the render settings stored after each render in the current slot. 9 | Here's a demo: [https://youtu.be/a3FI_n6vH64](https://youtu.be/a3FI_n6vH64) 10 | ## VRAIS Tools 11 | The VRAIS tools are meant to help users of our free VR viewer VRAIS setup their VR scenes and upload the finished rendering to [vrais.io.](http://www.vrais.io) 12 | ### Installation 13 | To install the vrais tools, simply download the script to your computer and install it as a Blender Addon from your User Preferences. 14 | In order to be able to upload to vrais.io you first need to copy the private API key from your Account Settings on vrais.io. Then open up the preferences of the addon and paste the user key there. 15 | The VRAIS Tools will appear in the Render section of the Properties editor in Blender. 16 | ### Usage 17 | This addon assumes that you use it from within your active scene. For instance: You finish a rendering in Scene_01, you press the cubemap or upload button in Scene_01. The renderpath and camera stereo convergence from Scene_01 will be used by the addon. 18 | ### Creating a cubemap 19 | Cubemaps have the advantage of making better use of the pixels in your VR rendering. If you render an equirectangular panorama the upper and lower thirds of the image will be 20 | unnecessarily oversampled, hence wasting precious render time. A cubemap avoids that. 21 | In order to render a cubemap you need to enable the Cube Map addon by Dalai Felinto. You find it in the Testing section of the Blender Addons. 22 | That addon will render 12 images, left and right eye for each of the 6 sides of a cubemap. To upload that to vrais.io you need to stitch 23 | these images to one long cubemap stripe. The "create cubemap" button will do that for you. 24 | ### Upload to vrais.io 25 | Once you have configured the addon by entering the user API key in the User Prefs, you can upload your finished rendering to vrais.io by clicking on the upload button. 26 | Before you do that make sure that you give your Rendering a title and a description. The stereo convergence will be automatically copied from your scene camera and transferred to vrais.io. 27 | 28 | -------------------------------------------------------------------------------- /cable_wizard.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 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. 7 | # 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. 12 | # 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. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Cable Wizard", 21 | "author": "Jonas Dichelle, Sebastian Koenig", 22 | "version": (1,1), 23 | "blender": (2, 79, 0), 24 | "location": "Tool", 25 | "description": "Generate Cables with various properties", 26 | "warning": "", 27 | "wiki_url": "", 28 | "category": "Tools" 29 | } 30 | 31 | 32 | 33 | import bpy 34 | from bpy.types import Menu, Panel, Operator 35 | import random 36 | from mathutils import Vector 37 | from numpy import mean 38 | from math import sqrt 39 | 40 | class VIEW3D_OT_cable_wizard(Operator): 41 | bl_idname = "object.cable_wizard" 42 | bl_label = "Cable Wizard" 43 | bl_options = {'REGISTER', 'UNDO'} 44 | bl_description = "Create cables by vertex group" 45 | 46 | prevent_double = bpy.props.BoolProperty( 47 | name="Prevent Double Cables", 48 | description="Prevents two cables to spawn from the same vertices, it only makes sense to have this on when you have random gravity enabled", 49 | default = True) 50 | 51 | iterations = bpy.props.IntProperty( 52 | name="Iterations", 53 | default=20, 54 | min=1, 55 | description="Amount of cables to generate" 56 | ) 57 | gravity = bpy.props.FloatProperty( 58 | name="Gravity", 59 | default=1.0, 60 | description="Defines the amount of hanging of the cable" 61 | ) 62 | random_gravity = bpy.props.FloatProperty( 63 | name="Random Gravity", 64 | default=1.0, 65 | description="Defines the amount of hanging of the cable" 66 | ) 67 | thickness = bpy.props.FloatProperty( 68 | name="Thickness", 69 | default=0.03, 70 | description="The maximum thickness of the cable" 71 | ) 72 | random_thickness = bpy.props.FloatProperty( 73 | name="Random Thickness", 74 | default=0.03, 75 | description="The maximum thickness of the cable" 76 | ) 77 | min_length = bpy.props.FloatProperty( 78 | name="Min Cable Length", 79 | default=2.0, 80 | description="The minimum length of a cable" 81 | ) 82 | max_length = bpy.props.FloatProperty( 83 | name="Max Cable Length", 84 | default=50.0, 85 | description="The maximum length of a cable" 86 | ) 87 | spread = bpy.props.FloatProperty( 88 | name="Spread", 89 | default=0.01, 90 | description="Move cable ends from the same source away from eachother" 91 | ) 92 | @classmethod 93 | def poll(cls, context): 94 | if context.object.cable_source == "GREASE": 95 | return True 96 | elif not context.scene.vertex_group: 97 | return False 98 | return context.active_object is not None 99 | 100 | 101 | @staticmethod 102 | def make_poly_line(vector_list, thickness): 103 | # first create the data for the curve 104 | curvedata = bpy.data.curves.new(name="Cable", type='CURVE') 105 | curvedata.dimensions = '3D' 106 | curvedata.fill_mode = 'FULL' 107 | curvedata.bevel_resolution = 2 108 | curvedata.bevel_depth = thickness 109 | # then create a new object with our curvedata 110 | obj = bpy.data.objects.new("Cable", curvedata) 111 | obj.location = (0,0,0) #object origin 112 | #finally linkt it into our scene 113 | bpy.context.scene.objects.link(obj) 114 | # now fill the data with a spline 115 | polyline = curvedata.splines.new('NURBS') 116 | polyline.points.add(len(vector_list)-1) 117 | for i in range(len(vector_list)): 118 | polyline.points[i].co = vector_list[i] 119 | 120 | polyline.order_u = len(polyline.points) 121 | polyline.use_endpoint_u = True 122 | 123 | def generate_point(self ,coordinate): 124 | w=1 125 | point1 = coordinate[0] + (self.spread*random.uniform(-1,1)) 126 | point2 = coordinate[1] + (self.spread*random.uniform(-1,1)) 127 | point3 = coordinate[2] + (self.spread*random.uniform(-1,1)) 128 | return (point1, point2, point3, w) 129 | 130 | def create_vector_list(self, context, thickness, rnd1_loc, rnd2_loc): 131 | random_gravity = self.gravity + self.random_gravity * random.uniform(0,1) 132 | spread = self.spread*random.uniform(-1,1) 133 | w = 1 134 | # contruct the position of the point in the middle 135 | mid_x = mean([rnd1_loc[0], rnd2_loc[0]]) + thickness * random.uniform(4,15) 136 | mid_y = mean([rnd1_loc[1], rnd2_loc[1]]) + thickness * random.uniform(4,15) 137 | mid_z = mean([rnd1_loc[2], rnd2_loc[2]]) - random_gravity 138 | # construct 4d Vectors with empty 4th value (w) 139 | mid_vert = (mid_x, mid_y, mid_z, w) 140 | rnd_vert_1 = self.generate_point(rnd1_loc) 141 | rnd_vert_2 = self.generate_point(rnd2_loc) 142 | # create a list with these 3 points 143 | vector_list = [rnd_vert_1, mid_vert, rnd_vert_2] 144 | return vector_list 145 | 146 | def get_grease_points(self, context): 147 | # grease pencil 148 | strokes = context.scene.grease_pencil.layers.active.active_frame.strokes 149 | 150 | rnd_stroke1 = random.choice(strokes) 151 | rnd_stroke2 = random.choice(strokes) 152 | 153 | point1 = rnd_stroke1.points 154 | point2 = rnd_stroke2.points 155 | 156 | rnd_point1 = random.choice(point1) 157 | rnd_point2 = random.choice(point2) 158 | 159 | rnd1 = rnd_point1.co 160 | rnd2 = rnd_point2.co 161 | return (rnd1, rnd2) 162 | 163 | def get_vertex_points(self, context): 164 | ob = context.active_object 165 | scene = context.scene 166 | v_group = ob.vertex_groups[scene.vertex_group] 167 | group_list = [] 168 | group_index = v_group.index 169 | # find the vertices that are part of our group 170 | for v in ob.data.vertices: 171 | for g in v.groups: 172 | if g.group == group_index: 173 | group_list.append(v) 174 | rnd1 = random.choice(group_list) 175 | rnd2 = random.choice(group_list) 176 | 177 | # defining w is needed to get a valid vector point for the curve vertices 178 | w = 1 179 | normal_local = rnd1.normal.to_4d() 180 | normal_local.w = 0 181 | normal_local = (ob.matrix_world * normal_local).to_3d() 182 | 183 | # get the absolute positions of the two random vertices 184 | pos = ob.matrix_world 185 | rnd1 = pos * rnd1.co 186 | rnd2 = pos * rnd2.co 187 | return (rnd1, rnd2) 188 | 189 | def execute(self, context): 190 | ob = context.active_object 191 | i=0 192 | while i < self.iterations: 193 | thickness = self.thickness + self.random_thickness * random.uniform(-1,1) 194 | if context.object.cable_source == "GREASE": 195 | rnd1 = self.get_grease_points(context)[0] 196 | rnd2 = self.get_grease_points(context)[1] 197 | elif context.object.cable_source == "VERTEX": 198 | rnd1 = self.get_vertex_points(context)[0] 199 | rnd2 = self.get_vertex_points(context)[1] 200 | 201 | distance = sqrt((rnd1[0]-rnd2[0])**2 + (rnd1[1]-rnd2[1])**2 +(rnd1[2]-rnd2[2])**2) 202 | if not distance < self.min_length and not distance > self.max_length: 203 | vector_list = self.create_vector_list(context, thickness, rnd1, rnd2) 204 | # now that we know the positions, create the cables 205 | self.make_poly_line(vector_list, thickness) 206 | # try to avoid creating the same curve twice during one iteration 207 | if self.prevent_double is True: 208 | try: 209 | point_list.remove(rnd1) 210 | point_list.remove(rnd2) 211 | except: 212 | pass 213 | i+=1 214 | return {'FINISHED'} 215 | 216 | 217 | class VIEW3D_OT_cable_edit(Operator): 218 | bl_idname = "object.cable_edit" 219 | bl_label = "Cable Edit" 220 | bl_options = {'REGISTER', 'UNDO'} 221 | bl_description = "Edit cables by created by Cable Wizard" 222 | 223 | gravity = bpy.props.FloatProperty( 224 | name="Gravity", 225 | default=0.0, 226 | description="Defines the amount of hanging of the cable" 227 | ) 228 | random_gravity = bpy.props.FloatProperty( 229 | name="Random Gravity", 230 | default=0.0, 231 | description="Defines the amount of hanging of the cable" 232 | ) 233 | thickness = bpy.props.FloatProperty( 234 | name="Thickness", 235 | default=0.0, 236 | min=0.0, 237 | description="The maximum thickness of the cable" 238 | ) 239 | random_thickness = bpy.props.FloatProperty( 240 | name="Random Thickness", 241 | default=0.0, 242 | min=0.0, 243 | description="The maximum thickness of the cable" 244 | ) 245 | @classmethod 246 | def poll(cls, context): 247 | for ob in context.selected_objects: 248 | if not ob.type == 'CURVE' or len(context.selected_objects)==0: 249 | return False 250 | else: 251 | return True 252 | 253 | def execute(self, context): 254 | obs = context.selected_objects 255 | for c in obs: 256 | thickness = self.thickness + self.random_thickness * random.uniform(-1,1) 257 | random_gravity = self.gravity + self.random_gravity * random.uniform(0,1) 258 | spline = c.data.splines[0] 259 | if len(spline.points) == 3: 260 | spline.points[1].co.z -= random_gravity 261 | c.data.bevel_depth = c.data.bevel_depth + thickness 262 | return {'FINISHED'} 263 | 264 | 265 | class VIEW3D_PT_cable_wizard(Panel): 266 | bl_label = "Cable Wizard" 267 | bl_idname = "object.cable_wizard_menu" 268 | bl_space_type = "VIEW_3D" 269 | bl_region_type = "TOOLS" 270 | bl_category = "Create" 271 | bl_context = "objectmode" 272 | 273 | @classmethod 274 | def poll(cls, context): 275 | return context.active_object 276 | 277 | 278 | def draw(self, context): 279 | scn = context.scene 280 | ob = context.object 281 | 282 | layout = self.layout 283 | layout.label("Cable Source:") 284 | row = layout.row() 285 | row.prop(ob, "cable_source", expand=True) 286 | col = layout.column() 287 | # this is very hacky. 288 | # if an object without v_group is active, the group field shows red 289 | # looks like an error, even if it's not. 290 | if ob.cable_source == 'VERTEX' and ob.type == 'MESH': 291 | col.prop_search(scn, "vertex_group", context.active_object, "vertex_groups", text="") 292 | col.label("Create Cables") 293 | col.operator("object.cable_wizard", icon="IPO_EASE_IN") 294 | col.label("Edit Cables") 295 | col.operator("object.cable_edit") 296 | 297 | 298 | 299 | 300 | classes = ( 301 | VIEW3D_OT_cable_wizard, 302 | VIEW3D_OT_cable_edit, 303 | VIEW3D_PT_cable_wizard, 304 | ) 305 | 306 | def register(): 307 | for c in classes: 308 | bpy.utils.register_class(c) 309 | bpy.types.Scene.vertex_group = bpy.props.StringProperty(name="vertex_group") 310 | 311 | bpy.types.Object.cable_source = bpy.props.EnumProperty( 312 | items=( 313 | ('VERTEX', "Vertex Group", ""), 314 | ('GREASE', "Grease Pencil", ""), 315 | ), 316 | default='VERTEX' 317 | ) 318 | def unregister(): 319 | for c in classes: 320 | bpy.utils.unregister_class(c) 321 | del bpy.types.Scene.vertex_group 322 | del bpy.types.Object.cable_source 323 | 324 | if __name__== "__main__": 325 | register() 326 | -------------------------------------------------------------------------------- /enable_gpus.py: -------------------------------------------------------------------------------- 1 | """ 2 | This will allow you to choose which GPUs to use with Commandline Rendering. 3 | Launch Blender with the script like this: 4 | 5 | $ blender -b -P enable_gpus.py blendfile.blend -a 6 | 7 | """ 8 | import bpy 9 | 10 | 11 | def enable_gpus(device_type, use_cpus=False, filter_by_name=None): 12 | """ 13 | Enable GPU rendering and configure GPUs. 14 | 15 | device_type: OPTIX or CUDA 16 | use_cpus: render with GPUs AND CPUS 17 | filter_by_name: Choose GPU(s) by name, e.g. "1080" or "3080" 18 | """ 19 | 20 | preferences = bpy.context.preferences 21 | cycles_preferences = preferences.addons["cycles"].preferences 22 | devices = bpy.context.preferences.addons["cycles"].preferences.get_devices_for_type(device_type) 23 | 24 | # use GPU rendering 25 | bpy.context.scene.cycles.device = "GPU" 26 | # OPTIX or CUDA? 27 | cycles_preferences.compute_device_type = device_type 28 | 29 | for device in devices: 30 | # first activate all devices 31 | device.use = True 32 | # disable devices that don't match the filter_by_name 33 | if filter_by_name and filter_by_name not in device.name: 34 | device.use = False 35 | # disable CPU if necessary 36 | if device.type == "CPU": 37 | device.use = use_cpus 38 | 39 | # return activated devices for printing to check if everything worked 40 | activated_devices = [d.name for d in devices if d.use] 41 | 42 | return activated_devices 43 | 44 | 45 | print(enable_gpus("OPTIX", filter_by_name="3080")) 46 | -------------------------------------------------------------------------------- /filter_foreground.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import Operator 3 | from mathutils import Vector 4 | 5 | def get_marker_coordinates_in_pixels(context, track, frame_number): 6 | width, height = context.space_data.clip.size 7 | # return the marker coordinates in relation to the clip 8 | marker = track.markers.find_frame(frame_number) 9 | vector = Vector((marker.co[0] * width, marker.co[1] * height)) 10 | return vector 11 | 12 | 13 | def marker_velocity(context, track, frame): 14 | marker_a = get_marker_coordinates_in_pixels(context, track, frame) 15 | marker_b = get_marker_coordinates_in_pixels(context, track, frame-1) 16 | marker_velocity = marker_a - marker_b 17 | return marker_velocity 18 | 19 | 20 | def get_difference(track_slope, average_slope, axis): 21 | # return the difference between slope of last frame and the average slope before it 22 | difference = track_slope[axis] - average_slope[axis] 23 | # rather use abs difference, to be able to better compare the actual value 24 | difference = abs(difference) 25 | return difference 26 | 27 | def get_slope(context, track, frame): 28 | v1 = marker_velocity(context, track, frame) 29 | v2 = marker_velocity(context, track, frame-1) 30 | slope = v1-v2 31 | return slope 32 | 33 | def check_evaluation_time(track, frame, evaluation_time): 34 | # check each frame for the evaluation time 35 | list = [] 36 | for f in range(frame-evaluation_time, frame): 37 | # if there are no markers for that frame, skip 38 | if not track.markers.find_frame(f): 39 | continue 40 | # it also doesnt make sense to use a track that is has no previous marker 41 | if not track.markers.find_frame(f-1): 42 | continue 43 | # the frame after the last track is muted, but still valid, so skip that 44 | if track.markers.find_frame(f).mute: 45 | continue 46 | if track.markers.find_frame(f-1).mute: 47 | continue 48 | # append frames to the list 49 | list.append(f) 50 | # make sure there are no gaps in the list 51 | if len(list) == evaluation_time: 52 | return True 53 | 54 | def get_valid_tracks(scene, tracks): 55 | valid_tracks = {} 56 | for t in tracks: 57 | list = [] 58 | for f in range(scene.frame_start, scene.frame_end): 59 | if not t.markers.find_frame(f): 60 | continue 61 | if t.markers.find_frame(f).mute: 62 | continue 63 | if not t.markers.find_frame(f-1): 64 | continue 65 | list.append(f) 66 | valid_tracks[t] = list 67 | return valid_tracks 68 | 69 | 70 | def get_average_slope(context, track, frame, evaluation_time): 71 | average = Vector().to_2d() 72 | for f in range(frame-evaluation_time, frame): 73 | average = get_slope(context, track, f) 74 | average += average 75 | average = average / evaluation_time 76 | return average 77 | 78 | 79 | def filter_track_ends(context, threshold, evaluation_time): 80 | # compare the last frame's slope with the ones before, and if needed, mute it. 81 | tracks = context.space_data.clip.tracking.tracks 82 | valid_tracks = get_valid_tracks(context.scene, tracks) 83 | to_clean = {} 84 | for track, list in valid_tracks.items(): 85 | f = list[-1] 86 | # first get the slope of the current track on current frame 87 | track_slope = get_slope(context, track, f) 88 | # if the track is as long as the evaluation time, calculate the average slope 89 | if check_evaluation_time(track, f, evaluation_time): 90 | average_slope = Vector().to_2d() 91 | for i in range(f-evaluation_time, f): 92 | # get the slopes of all frames during the evaluation time 93 | av_slope = get_slope(context, track, i) 94 | average_slope += av_slope 95 | average_slope = average_slope / evaluation_time 96 | # check abs difference for both values in the vector 97 | for i in [0,1]: 98 | # if the difference between average_slope and track_slope on any axis is above threshold, 99 | # add to the to_clean dictionary 100 | if not track in to_clean and get_difference(track_slope, average_slope, i) > threshold: 101 | to_clean[track] = f 102 | # now we can disable the last frame of the identified tracks 103 | for track, frame in to_clean.items(): 104 | print("cleaned ", track.name, "on frame ", frame) 105 | track.markers.find_frame(frame).mute=True 106 | return len(to_clean) 107 | 108 | 109 | def filter_foreground(context, evaluation_time, threshold): 110 | # filter tracks that move a lot faster than others towards the end of the track 111 | tracks = context.space_data.clip.tracking.tracks 112 | valid_tracks = get_valid_tracks(context.scene, tracks) 113 | foreground = [] 114 | for track, list in valid_tracks.items(): 115 | f = list[-1] 116 | # first get the average of the last frame during evaluation time 117 | if check_evaluation_time(track, f, evaluation_time) and not track in foreground: 118 | track_average = get_average_slope(context, track, f, evaluation_time) 119 | # then get the average of all other tracks 120 | global_average = Vector().to_2d() 121 | currently_valid_tracks = [] 122 | # first check if the other tracks are valid too. 123 | for t in tracks: 124 | if check_evaluation_time(t, f, evaluation_time) and not t == track: 125 | currently_valid_tracks.append(t) 126 | for t in currently_valid_tracks: 127 | other_average = get_average_slope(context, t, f, evaluation_time) 128 | global_average += other_average 129 | global_average = global_average / len(currently_valid_tracks) 130 | print(track.name, f, track_average, global_average) 131 | for i in [0,1]: 132 | difference = get_difference(track_average, global_average, i) * evaluation_time 133 | print(track.name, i, difference) 134 | if difference > threshold: 135 | foreground.append(track) 136 | for track in foreground: 137 | track.select = True 138 | 139 | 140 | class CLIP_OT_filter_track_ends(Operator): 141 | '''Filter the Track for spikes at the end of a track''' 142 | bl_idname = "clip.filter_track_ends" 143 | bl_label = "Filter Track Ends" 144 | bl_options = {'REGISTER', 'UNDO'} 145 | 146 | evaluation_time = bpy.props.IntProperty( 147 | name="Evaluation Time", 148 | default=10, 149 | min=0, 150 | max=1000, 151 | description="The length of the last part of the track that should be filtered") 152 | 153 | threshold = bpy.props.IntProperty( 154 | name="Threshold", 155 | default=1, 156 | min=0, 157 | max=100, 158 | description="The threshold over which a marker is considered outlier") 159 | 160 | @classmethod 161 | def poll(cls, context): 162 | sc = context.space_data 163 | return (sc.type == 'CLIP_EDITOR') and sc.clip 164 | 165 | def execute(self, context): 166 | # first do a minimal cleanup 167 | bpy.ops.clip.clean_tracks(frames=3, error=0, action='DELETE_SEGMENTS') 168 | num_tracks = filter_track_ends(context, self.threshold, self.evaluation_time) 169 | self.report({'INFO'}, "Muted %d track ends" % num_tracks) 170 | return {'FINISHED'} 171 | 172 | 173 | class CLIP_OT_filter_foreground(Operator): 174 | '''Filter Foreground Tracks with faster movement''' 175 | bl_idname = "clip.filter_foreground_track" 176 | bl_label = "Filter Foreground Tracks" 177 | bl_options = {'REGISTER', 'UNDO'} 178 | 179 | evaluation_time = bpy.props.IntProperty( 180 | name="Evaluation Time", 181 | default=20, 182 | min=0, 183 | max=1000, 184 | description="The length of the last part of the track that should be filtered") 185 | 186 | threshold = bpy.props.IntProperty( 187 | name="Threshold", 188 | default=2, 189 | min=0, 190 | max=100, 191 | description="The threshold over which a marker is considered outlier") 192 | 193 | @classmethod 194 | def poll(cls, context): 195 | sc = context.space_data 196 | return (sc.type == 'CLIP_EDITOR') and sc.clip 197 | 198 | def execute(self, context): 199 | scene = context.scene 200 | filter_foreground(context, self.evaluation_time, self.threshold) 201 | return {'FINISHED'} 202 | 203 | 204 | def register(): 205 | bpy.utils.register_class(CLIP_OT_filter_track_ends) 206 | bpy.utils.register_class(CLIP_OT_filter_foreground) 207 | 208 | def unregister(): 209 | bpy.utils.unregister_class(CLIP_OT_filter_track_ends) 210 | bpy.utils.unregister_class(CLIP_OT_filter_foreground) 211 | 212 | if __name__ == "__main__": 213 | register() 214 | -------------------------------------------------------------------------------- /grouping_pies.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 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. 7 | # 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. 12 | # 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. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Grouping Pies", 21 | "author": "Sebastian Koenig", 22 | "version": (0,1), 23 | "blender": (2, 79, 0), 24 | "location": "Viewport", 25 | "description": "Control Groups with a pie menu", 26 | "warning": "", 27 | "wiki_url": "", 28 | "category": "Object" 29 | } 30 | 31 | import bpy 32 | from bpy.types import Menu 33 | from bpy.types import Operator 34 | 35 | 36 | def check_if_group(context): 37 | # return True only if the object is part of a group 38 | if len(context.active_object.users_group) > 0: 39 | return True 40 | 41 | def check_if_empty(context): 42 | # check if the object is actually an Empty 43 | if context.active_object.type == 'EMPTY': 44 | return True 45 | 46 | def group_items(self, context): 47 | items = [(g.name, g.name, g.name) for g in bpy.data.groups] 48 | return items 49 | 50 | def assign_group(self, context): 51 | for ob in context.selected_objects: 52 | # if the group propery has not been written, set the group to None 53 | if not context.scene.group: 54 | ob.dupli_group = None 55 | else: 56 | # now that we know that there is scene.group, assign it to the active object 57 | group = bpy.data.groups[context.scene.group] 58 | ob.dupli_type = 'GROUP' 59 | ob.dupli_group = group 60 | 61 | 62 | class VIEW3D_OT_DupliOffset(Operator): 63 | """Set offset used for DupliGroup based on cursor position""" 64 | bl_idname = "object.dupli_offset_from_cursor" 65 | bl_label = "Set Offset From Cursor" 66 | bl_options = {'REGISTER', 'UNDO'} 67 | 68 | # i copied this class from a blender UI script from the web, should be in blender/release somewhere 69 | group_index = bpy.props.IntProperty( 70 | name="Group", 71 | description="Group index to set offset for", 72 | default=0, 73 | ) 74 | 75 | @classmethod 76 | def poll(cls, context): 77 | return (context.active_object is not None and check_if_group(context)) 78 | 79 | def execute(self, context): 80 | scene = context.scene 81 | ob = context.active_object 82 | group = self.group_index 83 | 84 | ob.users_group[group].dupli_offset = scene.cursor_location 85 | return {'FINISHED'} 86 | 87 | 88 | class VIEW3D_OT_NameGroupFromObject(Operator): 89 | ''' Set Group Name from Object Name (only if it's part of only 1 group)''' 90 | bl_idname = "object.name_group_from_object" 91 | bl_label = "GroupName from Object" 92 | 93 | @classmethod 94 | def poll(cls, context): 95 | return (context.object is not None and check_if_group(context)) 96 | 97 | def execute(self, context): 98 | ob = context.active_object 99 | # Only if there is one group the object is part of, rename it 100 | if not len(ob.users_group) > 1: 101 | ob.users_group[0].name = ob.name 102 | return {'FINISHED'} 103 | 104 | 105 | class VIEW3D_OT_NameObjectFromGroup(Operator): 106 | ''' Set Object Name from Group Name (only it it's part of only 1 group)''' 107 | bl_idname = "object.name_object_from_group" 108 | bl_label = "ObjectName from Group" 109 | 110 | @classmethod 111 | def poll(cls, context): 112 | return (context.object is not None and check_if_group(context)) 113 | 114 | def execute(self, context): 115 | # Only if there is one group the object is part of, rename it 116 | for ob in context.selected_objects: 117 | if not len(ob.users_group) > 1: 118 | ob.name = ob.users_group[0].name 119 | return {'FINISHED'} 120 | 121 | 122 | 123 | class VIEW3D_OT_AssignGroup(Operator): 124 | ''' Open a dialogue with Group Controls''' 125 | bl_label = "Group Select" 126 | bl_idname = "object.assign_group" 127 | bl_options = {'REGISTER', 'UNDO'} 128 | 129 | 130 | @classmethod 131 | def poll(cls, context): 132 | return (context.object is not None and check_if_empty(context)) 133 | 134 | def invoke(self, context, event): 135 | return context.window_manager.invoke_props_dialog(self) 136 | 137 | def draw(self, context): 138 | # Show the name of the active Group, if any 139 | ob = context.active_object 140 | if ob.dupli_group: 141 | current_group = context.active_object.dupli_group.name 142 | else: 143 | current_group = "No Group" 144 | # UI 145 | layout = self.layout 146 | layout.label(text=current_group) 147 | row = layout.row(align=True) 148 | # search the groups in bpy.data and then write it to scene.group 149 | row.prop_search(context.scene, "group", bpy.data, "groups") 150 | 151 | def execute(self, context): 152 | assign_group(self, context) 153 | return {'FINISHED'} 154 | 155 | 156 | class VIEW3D_OT_SetGroupDrawType(Operator): 157 | bl_idname = "object.set_group_draw_size" 158 | bl_label = "Group Draw Size" 159 | 160 | @classmethod 161 | def poll(cls, context): 162 | return (context.object is not None) 163 | 164 | def execute(self, context): 165 | ob = context.active_object 166 | if ob.type == 'EMPTY': 167 | ob.empty_draw_size = 0.1 168 | return {'FINISHED'} 169 | 170 | 171 | class VIEW3D_PIE_GroupingPies(Menu): 172 | bl_idname = "object.grouping_pies" 173 | bl_label = "Grouping Pies" 174 | 175 | def draw(self, context): 176 | layout = self.layout 177 | pie = layout.menu_pie() 178 | 179 | pie.operator("object.select_linked", text="Select Same Object Data", icon='OBJECT_DATA').type='OBDATA' 180 | pie.operator("object.select_linked", text="Select Same Group", icon='GROUP').type='DUPGROUP' 181 | pie.operator("object.edit_linked", icon='LINK_BLEND') 182 | pie.operator("object.assign_group", text="Assign Group", icon='GROUP') 183 | pie.operator("object.dupli_offset_from_cursor", text="Offset from Cursor", icon='CURSOR') 184 | pie.operator("object.set_group_draw_size", icon='EMPTY_DATA') 185 | pie.operator("object.name_object_from_group", icon='OUTLINER_OB_GROUP_INSTANCE') 186 | pie.operator("object.name_group_from_object", icon='MESH_CUBE') 187 | 188 | 189 | 190 | # ########################################################### 191 | # REGISTER 192 | # ########################################################### 193 | 194 | 195 | classes = ( 196 | VIEW3D_OT_AssignGroup, 197 | VIEW3D_PIE_GroupingPies, 198 | VIEW3D_OT_DupliOffset, 199 | VIEW3D_OT_SetGroupDrawType, 200 | VIEW3D_OT_NameGroupFromObject, 201 | VIEW3D_OT_NameObjectFromGroup 202 | ) 203 | 204 | def register(): 205 | for c in classes: 206 | bpy.utils.register_class(c) 207 | 208 | wm = bpy.context.window_manager 209 | km = wm.keyconfigs.addon.keymaps.new(name="Object Mode") 210 | kmi = km.keymap_items.new('wm.call_menu_pie', 'Y', 'PRESS', shift=True).properties.name = "object.grouping_pies" 211 | 212 | bpy.types.Scene.group = bpy.props.StringProperty() 213 | 214 | def unregister(): 215 | for c in classes: 216 | bpy.utils.unregister_class(c) 217 | 218 | del bpy.types.Scene.group 219 | 220 | 221 | if __name__ == "__main__": 222 | register() 223 | -------------------------------------------------------------------------------- /marker_weight.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 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. 7 | # 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. 12 | # 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. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Fade Marker Weight", 21 | "author": "Sebastian Koenig", 22 | "version": (0,1), 23 | "blender": (2, 79, 0), 24 | "location": "Clip Editor", 25 | "description": "Fade in the weight of tracking markers to smooth out the camera path", 26 | "warning": "", 27 | "wiki_url": "", 28 | "category": "Tracking" 29 | } 30 | 31 | import bpy 32 | from bpy.types import Operator, Panel 33 | 34 | def get_marker_list(scene, tracks): 35 | ''' 36 | Everytime the operator is executed, generate a dictionary with all tracks and 37 | their markers, if they are not too short and/or are selected 38 | ''' 39 | marker_dict = {} 40 | 41 | for t in tracks: 42 | # only operate on selected tracks that are not hidden 43 | if t.select and not t.hide: 44 | # generate a list of all tracked frames 45 | list = [] 46 | for i in range(scene.frame_start, scene.frame_end): 47 | # first clear the weight of the tracks 48 | t.keyframe_delete(data_path="weight", frame=i) 49 | if t.markers.find_frame(i): 50 | list.append(i) 51 | # if the list is longer than 20, add the list and the track to a dict 52 | # (a shorter list wouldn't make much sense) 53 | if len(list) > 20: 54 | marker_dict[t] = list 55 | return marker_dict 56 | 57 | 58 | def select_zero_weighted_tracks(scene, tracks): 59 | current_frame = scene.frame_current 60 | for t in tracks: 61 | t.select = True 62 | for f in range(scene.frame_start, scene.frame_end): 63 | scene.frame_set(f) 64 | for t in tracks: 65 | if t.weight>0: 66 | t.select = False 67 | scene.frame_current = current_frame 68 | 69 | 70 | def insert_keyframe(scene, fade_time, marker_dict): 71 | for track, list in marker_dict.items(): 72 | # define keyframe_values 73 | frame1 = list[0] 74 | frame2 = list[0] + fade_time 75 | frame3 = list[-2] - fade_time 76 | frame4 = list[-2] 77 | # only key track start if it is not the start of the clip 78 | if frame1 - scene.frame_start > fade_time: 79 | track.weight = 0 80 | track.keyframe_insert(data_path="weight", frame=frame1) 81 | track.weight = 1 82 | track.keyframe_insert(data_path="weight", frame=frame2) 83 | # now set keyframe for weight 0 at the end of the track 84 | # but only if it doesnt go until the end of the shot 85 | if scene.frame_end - frame4+1 > fade_time: 86 | track.keyframe_insert(data_path="weight", frame=frame3) 87 | track.weight = 0 88 | track.keyframe_insert(data_path="weight", frame=frame4) 89 | 90 | 91 | 92 | ############################## 93 | # CLASSES 94 | ############################## 95 | 96 | 97 | class CLIP_OT_SelectZeroWeightedTracks(Operator): 98 | '''Select all tracks that have a marker weight of zero through the entire shot''' 99 | bl_idname = "clip.select_zero_weighted_tracks" 100 | bl_label = "Select Zero Weighted Tracks" 101 | 102 | @classmethod 103 | def poll(cls, context): 104 | space = context.space_data 105 | return (space.type == 'CLIP_EDITOR') 106 | 107 | def execute(self, context): 108 | scene = context.scene 109 | tracks = context.space_data.clip.tracking.tracks 110 | select_zero_weighted_tracks(scene, tracks) 111 | return {'FINISHED'} 112 | 113 | 114 | class CLIP_OT_WeightFade(Operator): 115 | '''Fade in the weight of selected markers''' 116 | bl_idname = "clip.weight_fade" 117 | bl_label = "Fade Marker Weight" 118 | bl_options = {'REGISTER', 'UNDO'} 119 | 120 | fade_time = bpy.props.IntProperty(name="Fade Time", 121 | default=10, min=0, max=100) 122 | 123 | @classmethod 124 | def poll(cls, context): 125 | space = context.space_data 126 | return (space.type == 'CLIP_EDITOR') 127 | 128 | def execute(self, context): 129 | scene = context.scene 130 | tracks = context.space_data.clip.tracking.tracks 131 | insert_keyframe(scene, self.fade_time, get_marker_list(scene, tracks)) 132 | return {'FINISHED'} 133 | 134 | 135 | class CLIP_PT_WeightFadePanel(Panel): 136 | bl_idname = "clip.weight_fade_panel" 137 | bl_label = "Weight Fade" 138 | bl_space_type = "CLIP_EDITOR" 139 | bl_region_type = "TOOLS" 140 | bl_category = "Track" 141 | 142 | def draw(self, context): 143 | layout = self.layout 144 | col = layout.column() 145 | col.operator("clip.weight_fade") 146 | 147 | 148 | 149 | ################### 150 | # REGISTER 151 | ################### 152 | 153 | classes = ( 154 | CLIP_OT_WeightFade, 155 | CLIP_OT_SelectZeroWeightedTracks, 156 | CLIP_PT_WeightFadePanel 157 | ) 158 | 159 | def register(): 160 | for c in classes: 161 | bpy.utils.register_class(c) 162 | 163 | wm = bpy.context.window_manager 164 | km = wm.keyconfigs.addon.keymaps.new(name='Clip Editor', space_type='CLIP_EDITOR') 165 | kmi = km.keymap_items.new('clip.weight_fade', 'W', 'PRESS', alt=True) 166 | 167 | def unregister(): 168 | for c in classes: 169 | bpy.utils.unregister_class(c) 170 | 171 | if __name__ == "__main__": 172 | register() 173 | -------------------------------------------------------------------------------- /masking_pies.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Masking Pies", 3 | "author": "Sebastian Koenig", 4 | "version": (1, 0), 5 | "blender": (2, 7, 2), 6 | "location": "Clip Editor > Masking Pies", 7 | "description": "Pie Controls for Masking", 8 | "warning": "", 9 | "wiki_url": "", 10 | "tracker_url": "", 11 | "category": "Compositing"} 12 | 13 | 14 | 15 | import bpy 16 | from bpy.types import Menu, Operator 17 | 18 | 19 | def set_mask_mode(context, mask): 20 | # Set mask mode in Image and Clip Editor 21 | scn = context.scene 22 | active_node = scn.node_tree.nodes.active 23 | # set mode to mask and select the mask 24 | for area in context.screen.areas: 25 | active_space = area.spaces.active 26 | if area.type == 'IMAGE_EDITOR' or area.type == 'CLIP_EDITOR': 27 | active_space.mode = 'MASK' 28 | active_space.mask = mask 29 | # if it is the image editor, assign the viewer node if possible 30 | elif area.type == 'IMAGE_EDITOR': 31 | try: 32 | active_space.image = bpy.data.images["Viewer Node"] 33 | except: 34 | print("There is no Viewer Node yet") 35 | 36 | 37 | def selected_mask_points(context): 38 | mask = context.space_data.mask 39 | pointlist = [] 40 | try: 41 | for l in mask.layers: 42 | if not l.hide and not l.hide_select: 43 | for s in l.splines: 44 | for p in s.points: 45 | if p.select: 46 | pointlist.append(p) 47 | return pointlist 48 | except: 49 | print("no points selected?") 50 | 51 | 52 | def add_mask_to_node(context, clip, target_input): 53 | # add a new mask node and configure it 54 | tree = context.scene.node_tree 55 | links = tree.links 56 | active_node = tree.nodes.active 57 | new_mask_node = tree.nodes.new(type="CompositorNodeMask") 58 | next_node_location = (active_node.location[0]-200, active_node.location[1]+100) 59 | if clip: 60 | if clip.tracking.camera.pixel_aspect != 1: 61 | scale_node = tree.nodes.new(type="CompositorNodeScale") 62 | scale_node.space = 'RELATIVE' 63 | scale_node.inputs[2].default_value = clip.tracking.camera.pixel_aspect 64 | scale_node.location = next_node_location 65 | new_mask_node.location = scale_node.location[0] - 200, active_node.location[1] + 100 66 | links.new(scale_node.outputs[0], target_input) 67 | links.new(new_mask_node.outputs[0], scale_node.inputs[0]) 68 | else: 69 | new_mask_node.location = active_node.location[0] - 200, active_node.location[1] + 100 70 | links.new(new_mask_node.outputs[0], target_input) 71 | mask = bpy.data.masks.new(name="") 72 | new_mask_node.mask = mask 73 | 74 | 75 | class NODE_OT_add_mask_to_node(Operator): 76 | bl_idname = "node.add_mask_to_node" 77 | bl_label = "Add Mask to Node" 78 | 79 | @classmethod 80 | def poll(cls, context): 81 | space = context.space_data 82 | return space.type == 'NODE_EDITOR' and space.tree_type == 'CompositorNodeTree' 83 | 84 | def execute(self, context): 85 | scn = context.scene 86 | tree = scn.node_tree 87 | links = tree.links 88 | clip = scn.active_clip 89 | 90 | active_node = tree.nodes.active 91 | if active_node: 92 | if active_node.inputs.get('Fac'): 93 | fac_input = active_node.inputs['Fac'] 94 | if not len(fac_input.links) > 0: 95 | add_mask_to_node(context, clip, fac_input) 96 | else: 97 | print("the node ", active_node.name, "already has a node linked to Fac input") 98 | elif active_node.type == 'MATH': 99 | # this is really ugly, but works 100 | input_list = [] 101 | for input in active_node.inputs: 102 | if not len(input.links) > 0: 103 | input_list.append(input) 104 | if len(input_list) == 2: 105 | fac_input = input_list[0] 106 | add_mask_to_node(context, clip, fac_input) 107 | elif len(input_list) == 1: 108 | fac_input = input_list[0] 109 | add_mask_to_node(context, clip, fac_input) 110 | else: 111 | print("No valid node active") 112 | return {'FINISHED'} 113 | 114 | 115 | class MASK_OT_activate_mask(Operator): 116 | """Make the active mask node the active mask in Clip/Image Editor""" 117 | bl_idname = "mask.activate_mask" 118 | bl_label = "Activate Mask" 119 | 120 | @classmethod 121 | def poll(cls, context): 122 | space = context.space_data 123 | return space.type == 'NODE_EDITOR' and space.tree_type == 'CompositorNodeTree' 124 | 125 | def execute(self, context): 126 | scn = context.scene 127 | active_node = scn.node_tree.nodes.active 128 | 129 | if active_node.type == 'MASK': 130 | the_mask = active_node.mask 131 | print(the_mask) 132 | set_mask_mode(context, the_mask) 133 | return {'FINISHED'} 134 | 135 | 136 | class MASK_OT_new_mask_layer(Operator): 137 | """Create a new masklayer, only edit this one""" 138 | bl_idname = "mask.new_mask" 139 | bl_label = "New Masklayer" 140 | 141 | def execute(self, context): 142 | mask = context.space_data.mask 143 | for l in mask.layers: 144 | l.hide_select = True 145 | bpy.ops.mask.layer_new() 146 | return {'FINISHED'} 147 | 148 | 149 | class MASK_OT_enable_self_intersection_check(Operator): 150 | """Enable Self Intersection Check of active spline""" 151 | bl_idname = "mask.enable_self_intersection_check" 152 | bl_label = "Enable Self-Intersection Check" 153 | 154 | def execute(self, context): 155 | mask = context.space_data.mask 156 | for s in mask.layers.active.splines: 157 | s.use_self_intersection_check = True 158 | return {'FINISHED'} 159 | 160 | 161 | class MASK_OT_set_to_add(Operator): 162 | """Set the current layer to Add""" 163 | bl_idname = "mask.set_add" 164 | bl_label = "Set Add" 165 | 166 | def execute(self, context): 167 | mask = context.space_data.mask 168 | active_layer = mask.layers.active 169 | active_layer.blend="MERGE_ADD" 170 | return {'FINISHED'} 171 | 172 | 173 | class MASK_OT_set_to_subtract(Operator): 174 | """Set current layer to Subtract""" 175 | bl_idname = "mask.set_subtract" 176 | bl_label = "Set Subtract" 177 | 178 | def execute(self, context): 179 | mask = context.space_data.mask 180 | active_layer = mask.layers.active 181 | active_layer.blend = "MERGE_SUBTRACT" 182 | return {'FINISHED'} 183 | 184 | 185 | class MASK_OT_toggle_drawtype(Operator): 186 | """Set the drawtype to smooth and toggle between white and outline""" 187 | bl_idname = "mask.toggle_drawtype" 188 | bl_label = "Toggle Drawtype" 189 | 190 | def execute(self, context): 191 | sc = context.space_data 192 | sc.show_mask_smooth = True 193 | if sc.mask_draw_type == "OUTLINE": 194 | sc.mask_draw_type = "WHITE" 195 | else: 196 | sc.mask_draw_type = "OUTLINE" 197 | return {'FINISHED'} 198 | 199 | 200 | class MASK_OT_set_marker_drawtype(Operator): 201 | """Don't draw markers""" 202 | bl_idname = "mask.set_marker_drawtype" 203 | bl_label = "Set Marker Drawtype" 204 | 205 | def execute(self, context): 206 | sc = context.space_data 207 | sc.show_marker_pattern = False 208 | sc.show_marker_search= False 209 | sc.show_track_path = False 210 | return {'FINISHED'} 211 | 212 | 213 | class MASK_OT_clear_keyframes(Operator): 214 | """Clear all keyframes of current mask""" 215 | bl_idname = "mask.clear_keyframes" 216 | bl_label = "Clear keyframes" 217 | bl_options = {'REGISTER', 'UNDO'} 218 | 219 | def execute(self, context): 220 | scn = context.scene 221 | current_frame = scn.frame_current 222 | # we wanna make sure that we stay on the current state of the mask (frame) 223 | for f in range(scn.frame_current+1, scn.frame_end+1): 224 | scn.frame_set(f) 225 | bpy.ops.mask.shape_key_clear() 226 | for f in range(scn.frame_start, scn.frame_current-2): 227 | scn.frame_set(f) 228 | bpy.ops.mask.shape_key_clear() 229 | scn.frame_current = current_frame 230 | return {'FINISHED'} 231 | 232 | 233 | class MASK_OT_isolate_layer(Operator): 234 | """Isolate the currently selected layer""" 235 | bl_idname = "mask.isolate_layer" 236 | bl_label = "Isolate Mask Layer" 237 | 238 | def execute(self, context): 239 | mask = context.space_data.mask 240 | active_layer = mask.layers.active 241 | for ml in mask.layers: 242 | ml.hide_select = True 243 | active_layer.hide_select = False 244 | return {'FINISHED'} 245 | 246 | 247 | class MASK_OT_switch_editor(bpy.types.Operator): 248 | """Toggle between Image and Clip Editor, while keeping same mode and display aspect""" 249 | bl_idname = "mask.switch_editor" 250 | bl_label = "Switch Editor" 251 | 252 | def execute(self, context): 253 | scn = context.scene 254 | for area in context.screen.areas: 255 | active_space = area.spaces.active 256 | if area.type == "CLIP_EDITOR": 257 | if scn.node_tree and scn.node_tree.nodes.get("Viewer"): 258 | mask = active_space.mask 259 | area.type = "IMAGE_EDITOR" 260 | active_area = area.spaces.active 261 | # this only works if a Viewer Node exists 262 | if bpy.data.images["Viewer Node"]: 263 | active_area.image = bpy.data.images["Viewer Node"] 264 | active_area.image.display_aspect[0] = scn.active_clip.tracking.camera.pixel_aspect 265 | active_area.mode = "MASK" 266 | active_area.mask = mask 267 | else: 268 | self.report({"INFO"}, "You need a Viewer Node for this to work") 269 | 270 | elif area.type == "IMAGE_EDITOR": 271 | mask = active_space.mask 272 | area.type = "CLIP_EDITOR" 273 | active_area = area.spaces.active 274 | active_area.mode = "MASK" 275 | active_area.mask = mask 276 | 277 | return {'FINISHED'} 278 | 279 | 280 | class MASK_OT_parent_marker_visibility(Operator): 281 | """Toggle visibility of parent markers of selected points""" 282 | bl_idname = "mask.parent_marker_visibility" 283 | bl_label = "Parent Marker Visibilty" 284 | bl_options = {'REGISTER', 'UNDO'} 285 | 286 | def execute(self, context): 287 | markers = set() # Using a set helps to avoid double entries 288 | for p in selected_mask_points(context): 289 | if p.parent.sub_parent: 290 | markers.add(p.parent.sub_parent) 291 | for m in markers: 292 | track = context.space_data.clip.tracking.objects.active.tracks[m] 293 | if track.hide == True: 294 | track.hide = False 295 | else: 296 | track.hide = True 297 | return {'FINISHED'} 298 | 299 | 300 | class MASK_OT_toggle_marker_visibility(Operator): 301 | """Toggle visibility of all markers of current clip""" 302 | bl_idname = "mask.toggle_marker_visibility" 303 | bl_label = "Toggle Marker Visibility" 304 | bl_options = {'REGISTER', 'UNDO'} 305 | 306 | @classmethod 307 | def poll(cls, context): 308 | space = context.space_data 309 | return space.type == 'CLIP_EDITOR' 310 | 311 | def execute(self, context): 312 | hidden = False 313 | tracks = context.space_data.clip.tracking.objects.active.tracks 314 | for t in tracks: 315 | if t.hide == True: 316 | hidden = True 317 | if hidden: 318 | for t in tracks: 319 | t.hide = False 320 | else: 321 | for t in tracks: 322 | t.hide = True 323 | return {'FINISHED'} 324 | 325 | 326 | class MASK_PIE_mask_editing(Menu): 327 | # label is displayed at the center of the pie menu. 328 | bl_label = "Masking Pie" 329 | bl_idname = "mask.mask_editing_pie" 330 | 331 | def draw(self, context): 332 | layout = self.layout 333 | pie = layout.menu_pie() 334 | pie.operator("mask.clear_keyframes", icon='SPACE3') 335 | pie.operator("mask.switch_direction", icon="ARROW_LEFTRIGHT") 336 | pie.operator("mask.cyclic_toggle", icon="CURVE_BEZCIRCLE") 337 | pie.operator("mask.handle_type_set", icon='IPO_BEZIER') 338 | pie.operator("mask.feather_weight_clear", icon='X') 339 | pie.operator("transform.transform", text="Scale Feather", icon="MAN_SCALE").mode = 'MASK_SHRINKFATTEN' 340 | pie.operator("mask.shape_key_feather_reset", icon='ANIM') 341 | pie.operator("mask.enable_self_intersection_check") 342 | # pie.prop(context.scene.tool_settings, "use_keyframe_insert_auto",text="Enable Autokey") 343 | 344 | 345 | class MASK_PIE_mask_layers(Menu): 346 | # label is displayed at the center of the pie menu. 347 | bl_label = "Masking Pie" 348 | bl_idname = "mask.mask_layer_pie" 349 | 350 | def draw(self, context): 351 | active_layer = context.space_data.mask.layers.active 352 | layout = self.layout 353 | pie = layout.menu_pie() 354 | 355 | pie.operator("mask.new_mask", icon='MESH_PLANE') 356 | pie.operator("mask.switch_editor", icon='IMAGE_COL') 357 | pie.operator("mask.isolate_layer", icon='RESTRICT_SELECT_OFF') 358 | pie.prop(active_layer, "invert", text="Invert Layer", icon='IMAGE_ALPHA') 359 | pie.operator("mask.set_subtract", icon='ZOOMOUT') 360 | pie.operator("mask.set_add", icon='ZOOMIN') 361 | pie.operator("mask.select_linked", icon="LINKED") 362 | #pie.operator("mask.lock_inactive_layers", icon='CONSTRAINT') 363 | 364 | 365 | class MASK_PIE_mask_display(Menu): 366 | bl_idname = "mask.mask_display_pie" 367 | bl_label = "Mask Display Options" 368 | 369 | def draw(self, context): 370 | layout = self.layout 371 | pie = layout.menu_pie() 372 | 373 | pie.operator("mask.toggle_marker_visibility", icon="VISIBLE_IPO_ON") 374 | pie.operator("mask.parent_marker_visibility", icon="GHOST_ENABLED") 375 | pie.operator("mask.toggle_drawtype", icon='CONSTRAINT') 376 | pie.prop(context.space_data, "show_mask_overlay", text="Show Mask Overlay", icon ="IMAGE_ZDEPTH") 377 | 378 | 379 | 380 | 381 | ########## register ############ 382 | classes = ( 383 | MASK_PIE_mask_editing, 384 | MASK_PIE_mask_layers, 385 | MASK_PIE_mask_display, 386 | NODE_OT_add_mask_to_node, 387 | MASK_OT_new_mask_layer, 388 | MASK_OT_set_to_subtract, 389 | MASK_OT_set_to_add, 390 | MASK_OT_toggle_drawtype, 391 | MASK_OT_set_marker_drawtype, 392 | MASK_OT_isolate_layer, 393 | MASK_OT_clear_keyframes, 394 | MASK_OT_switch_editor, 395 | MASK_OT_parent_marker_visibility, 396 | MASK_OT_toggle_marker_visibility, 397 | MASK_OT_enable_self_intersection_check, 398 | MASK_OT_activate_mask 399 | ) 400 | 401 | def register(): 402 | for c in classes: 403 | bpy.utils.register_class(c) 404 | 405 | wm = bpy.context.window_manager 406 | km = wm.keyconfigs.addon.keymaps.new(name='Mask Editing') 407 | 408 | kmi = km.keymap_items.new('wm.call_menu_pie', 'E', 'PRESS').properties.name = "mask.mask_editing_pie" 409 | kmi = km.keymap_items.new('wm.call_menu_pie', 'Q', 'PRESS').properties.name = "mask.mask_layer_pie" 410 | kmi = km.keymap_items.new('wm.call_menu_pie', 'W', 'PRESS').properties.name = "mask.mask_display_pie" 411 | 412 | km = wm.keyconfigs.addon.keymaps.new(name='Node Editor', space_type='NODE_EDITOR') 413 | kmi = km.keymap_items.new('mask.activate_mask', 'ACTIONMOUSE', 'DOUBLE_CLICK') 414 | kmi = km.keymap_items.new('node.add_mask_to_node', 'M', 'PRESS', ctrl=True) 415 | 416 | 417 | 418 | def unregister(): 419 | for c in classes: 420 | bpy.utils.unregister_class(c) 421 | 422 | if __name__ == "__main__": 423 | register() 424 | -------------------------------------------------------------------------------- /movieclip_3d_reconstruction.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 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. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # All rights reserved. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | bl_info = { 21 | "name": "Reconstruct 3D Mesh", 22 | "author": "Sebastian Koenig", 23 | "version": (1, 0), 24 | "blender": (2, 8, 0), 25 | "location": "Clip Editor > Tools > Reconstruct 3D Mesh", 26 | "description": "Generate a 3D mesh from trackers, works best for simple planes", 27 | "warning": "", 28 | "wiki_url": "", 29 | "tracker_url": "", 30 | "category": "Motion Tracking" 31 | } 32 | 33 | 34 | import bpy 35 | from bpy.types import Operator, Panel 36 | 37 | class CLIP_OT_mesh_reconstruction(Operator): 38 | ''' Create a face from selected tracks. Needs a camera solve. Works best for flat surfaces''' 39 | bl_idname = "clip.mesh_reconstruction" 40 | bl_label = "Mesh Reconstruction" 41 | bl_options = {'UNDO', 'REGISTER'} 42 | 43 | @classmethod 44 | def poll(cls, context): 45 | space = context.space_data 46 | return space.type == "CLIP_EDITOR" 47 | 48 | def execute(self, context): 49 | # make a mesh from selected markers, called "Tracks" 50 | bpy.ops.clip.bundles_to_mesh() 51 | 52 | # create a plane from the single vertices 53 | ob = bpy.data.objects["Tracks"] 54 | bpy.context.view_layer.objects.active = ob 55 | ob.select_set(True) 56 | bpy.ops.object.mode_set(mode="EDIT") 57 | bpy.ops.mesh.select_all(action="SELECT") 58 | bpy.ops.mesh.edge_face_add() 59 | bpy.ops.object.mode_set(mode="OBJECT") 60 | # rename the object so that you can create new objects (called "Tracks") 61 | ob.name = "TrackMesh" 62 | return {'FINISHED'} 63 | 64 | 65 | class CLIP_PT_mesh_reconstruction(bpy.types.Panel): 66 | bl_idname = "clip.mesh_reconstruction" 67 | bl_label = "Mesh Reconstruction" 68 | bl_space_type = "CLIP_EDITOR" 69 | bl_region_type = "TOOLS" 70 | bl_category = "Solve" 71 | 72 | def draw(self, context): 73 | layout = self.layout 74 | col = layout.column() 75 | col.operator("clip.mesh_reconstruction") 76 | 77 | 78 | ########## REGISTER ############ 79 | 80 | def register(): 81 | bpy.utils.register_class(CLIP_OT_mesh_reconstruction) 82 | bpy.utils.register_class(CLIP_PT_mesh_reconstruction) 83 | 84 | 85 | def unregister(): 86 | bpy.utils.unregister_class(CLIP_OT_mesh_reconstruction) 87 | bpy.utils.unregister_class(CLIP_PT_mesh_reconstruction) 88 | 89 | if __name__ == "__main__": 90 | register() 91 | -------------------------------------------------------------------------------- /nla_player.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import Menu, Operator, Panel 3 | 4 | 5 | class VP_OT_action_starter(Operator): 6 | bl_label = "Action Starter" 7 | bl_idname = "scene.action_starter" 8 | 9 | force_reset: bpy.props.BoolProperty(default=False) 10 | 11 | def execute(self, context): 12 | if not self.force_reset: 13 | frame = bpy.context.scene.frame_current 14 | else: 15 | frame = 5000 16 | # OBJECTS 17 | for ob in bpy.data.objects: 18 | if ob.pass_index == 10: 19 | continue 20 | if not ob.animation_data: 21 | continue 22 | if not ob.animation_data.action: 23 | continue 24 | for fcurve in ob.animation_data.action.fcurves: 25 | for point in fcurve.keyframe_points: 26 | point.co.x = frame 27 | 28 | # LAMPS 29 | for l in bpy.data.lights: 30 | if not l.animation_data: 31 | continue 32 | if not l.animation_data.action: 33 | continue 34 | for fcurve in l.animation_data.action.fcurves: 35 | for point in fcurve.keyframe_points: 36 | point.co.x = frame 37 | # SHAPEKEYS 38 | shapes = [] 39 | for me in bpy.data.meshes: 40 | shapes.append(me) 41 | for me in bpy.data.curves: 42 | shapes.append(me) 43 | for me in shapes: 44 | if not me.shape_keys: 45 | continue 46 | if not me.shape_keys.animation_data: 47 | continue 48 | for fcurve in me.shape_keys.animation_data.action.fcurves: 49 | for point in fcurve.keyframe_points: 50 | point.co.x = frame 51 | 52 | # SHADERNODES 53 | for ob in bpy.data.objects: 54 | if not ob.type == 'MESH': 55 | continue 56 | if not ob.data.materials: 57 | continue 58 | for mat in ob.data.materials: 59 | if not mat: 60 | continue 61 | if not mat.use_nodes: 62 | continue 63 | if not mat.node_tree.animation_data: 64 | continue 65 | if not mat.node_tree.animation_data.action: 66 | continue 67 | for fcurve in mat.node_tree.animation_data.action.fcurves: 68 | for point in fcurve.keyframe_points: 69 | point.co.x = frame 70 | 71 | return {'FINISHED'} 72 | 73 | 74 | class VIEW_3D_PT_action_starter(Panel): 75 | bl_label = "Start Action" 76 | bl_space_type = 'VIEW_3D' 77 | bl_region_type = 'UI' 78 | bl_category = 'VR' 79 | bl_context = 'objectmode' 80 | 81 | def draw(self, context): 82 | scene = context.scene 83 | layout = self.layout 84 | layout.use_property_split = True 85 | layout.use_property_decorate = False 86 | 87 | row = layout.row() 88 | col = row.column() 89 | ob = context.object 90 | col.operator("scene.action_starter").force_reset = False 91 | col.operator("scene.action_starter", text="Reset").force_reset = True 92 | 93 | classes = ( 94 | VP_OT_action_starter, 95 | VIEW_3D_PT_action_starter 96 | ) 97 | 98 | def register(): 99 | for c in classes: 100 | bpy.utils.register_class(c) 101 | 102 | # keymap 103 | wm = bpy.context.window_manager 104 | kc = wm.keyconfigs.addon 105 | if kc: 106 | km = kc.keymaps.new(name='3D View', space_type= 'VIEW_3D') 107 | # kmi = km.keymap_items.new("scene.action_starter", type='J', value='PRESS') 108 | # kmi = km.keymap_items.new("scene.vp_stop_recording", type='L', value='PRESS') 109 | # addon_keymaps.append((km, kmi)) 110 | 111 | def unregister(): 112 | for c in classes: 113 | bpy.utils.unregister_class() 114 | 115 | if __name__ == "__main__": 116 | register() 117 | 118 | -------------------------------------------------------------------------------- /render_tweaker.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 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. 7 | # 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. 12 | # 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. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Render Tweaker", 21 | "author": "Sebastian Koenig", 22 | "version": (1,1), 23 | "blender": (2, 77, 0), 24 | "location": "Properties > Render", 25 | "description": "Store Cycles rendersettings in render slots for easier tweaking", 26 | "warning": "", 27 | "wiki_url": "", 28 | "category": "Render" 29 | } 30 | 31 | 32 | import bpy 33 | from bpy.props import StringProperty, BoolProperty, IntProperty 34 | from bpy.utils import register_class, unregister_class 35 | from bl_operators.presets import AddPresetBase 36 | 37 | # ################################################ 38 | # FUNCTIONS ###################################### 39 | # ################################################ 40 | 41 | 42 | def cycles_exists(): 43 | return hasattr(bpy.types.Scene, "cycles") 44 | 45 | def get_slot_id(): 46 | return bpy.data.images['Render Result'].render_slots.active_index 47 | 48 | def enable_slot_recording(): 49 | context.scene.record_settings = True 50 | 51 | def return_proplist(): 52 | proplist = [ 53 | "aa_samples", 54 | "ao_bounces_render", 55 | "ao_samples", 56 | "blur_glossy", 57 | "caustics_reflective", 58 | "caustics_refractive", 59 | "dicing_rate", 60 | "diffuse_bounces", 61 | "diffuse_samples", 62 | "film_exposure", 63 | "film_transparent", 64 | "filter_type", 65 | "filter_width", 66 | "glossy_bounces", 67 | "glossy_samples", 68 | "light_sampling_threshold", 69 | "max_bounces", 70 | "max_subdivisions", 71 | "mesh_light_samples", 72 | "motion_blur_position", 73 | "pixel_filter_type", 74 | "progressive", 75 | "rolling_shutter_type", 76 | "rolling_shutter_duration", 77 | "sample_clamp_direct", 78 | "sample_clamp_indirect", 79 | "sample_all_lights_indirect", 80 | "sample_all_lights_direct", 81 | "samples", 82 | "sampling_pattern", 83 | "transmission_bounces", 84 | "subsurface_samples", 85 | "transmission_samples", 86 | "transparent_max_bounces", 87 | "use_square_samples", 88 | "volume_bounces", 89 | "volume_max_steps", 90 | "volume_samples", 91 | "volume_step_size" 92 | ] 93 | return proplist 94 | 95 | 96 | # save all visibly relevant cycles scene settings 97 | def save_settings_to_storage(slot_id): 98 | context = bpy.context 99 | scene = context.scene 100 | proplist = return_proplist() 101 | 102 | # if the dict doesnt exist yet, create it. 103 | if not scene.get('renderslot_properties'): 104 | scene['renderslot_properties'] = {} 105 | renderslot_properties = scene['renderslot_properties'] 106 | # get the active slot id (unless it is 8) 107 | if slot_id == 8: 108 | slot_id = str(slot_id) 109 | else: 110 | slot_id = str(get_slot_id()) 111 | bpy.context.window_manager.recent_render = str(int(slot_id)+1) 112 | # create the dict for the slot 113 | slot_id_dict = {} 114 | # fill the dict with the properties 115 | for prop in proplist: 116 | slot_id_dict[prop] = getattr(context.scene.cycles, prop) 117 | # assign the prop dict to the slot id as value 118 | renderslot_properties[slot_id] = slot_id_dict 119 | 120 | 121 | # load cycles render settings 122 | def load_settings_from_storage(context, slot_id): 123 | scene = context.scene 124 | try: 125 | for s in bpy.data.scenes: 126 | if s.master_scene: 127 | master = s 128 | renderslot_properties = master.get('renderslot_properties') 129 | # find the active slot id 130 | if not slot_id == 8: 131 | slot_id = str(get_slot_id()) 132 | else: 133 | slot_id = str(slot_id) 134 | # get the dict for that id 135 | prop_dict = renderslot_properties[slot_id] 136 | # read out the properties from the script and set them 137 | for prop in prop_dict: 138 | new_value = prop_dict[prop] 139 | setattr(scene.cycles, prop, new_value) 140 | return True 141 | except: 142 | return False 143 | 144 | 145 | 146 | # if slot recording is enabled, save render settings from current slot 147 | def slot_handler(scene): 148 | if scene.record_settings: 149 | save_settings_to_storage(0) 150 | 151 | 152 | 153 | # ########################################### 154 | # OPERATORS ################################# 155 | # ########################################### 156 | 157 | 158 | class RENDER_TWEAKER_OT_save_main_rendersettings(bpy.types.Operator): 159 | '''Save the current render settings as main settings''' 160 | bl_idname = "scene.save_main_rendersettings" 161 | bl_label = "Save Main Rendersettings" 162 | 163 | def execute(self, context): 164 | scene = context.scene 165 | # Make this the only master scene 166 | # (There can be only one! ;) 167 | for s in bpy.data.scenes: 168 | if s == scene: 169 | s.master_scene = True 170 | else: 171 | s.master_scene = False 172 | # Save the settings to an artificial 9th slot 173 | save_settings_to_storage(8) 174 | return {'FINISHED'} 175 | 176 | 177 | 178 | class RENDER_TWEAKER_OT_restore_main_rendersettings(bpy.types.Operator): 179 | '''Restore render settings from main settings''' 180 | bl_idname = "scene.restore_main_rendersettings" 181 | bl_label = "Restore Main Rendersettings" 182 | 183 | def execute(self, context): 184 | if not load_settings_from_storage(context,8): 185 | self.report({'ERROR'}, "Looks like you didn't save the main render setup yet!") 186 | return {'FINISHED'} 187 | 188 | 189 | 190 | class RENDER_TWEAKER_OT_enable_slot_recording(bpy.types.Operator): 191 | ''' Enable render setting storing. Press Ctrl+J to restore settings.''' 192 | bl_idname = "scene.enable_slot_recording" 193 | bl_label = "Record Render Settings" 194 | 195 | def execute(self, context): 196 | scene = context.scene 197 | if bpy.data.images.get('Render Result'): 198 | if not scene.record_settings: 199 | scene.record_settings = True 200 | save_settings_to_storage(0) 201 | else: 202 | scene.record_settings = False 203 | else: 204 | self.report({'WARNING'}, "You need to have a Render Result first.") 205 | return {'FINISHED'} 206 | 207 | 208 | 209 | class RENDER_TWEAKER_OT_render_slot_restore(bpy.types.Operator): 210 | '''Restore render settings from render slot''' 211 | bl_idname = "scene.render_slot_restore" 212 | bl_label = "Restore Rendersettings" 213 | 214 | def execute(self, context): 215 | if not load_settings_from_storage(context, 0): 216 | self.report({'ERROR'}, "Looks like render slot recording was not enabled. (Image Editor > Header)") 217 | return {'FINISHED'} 218 | 219 | 220 | 221 | 222 | class RENDER_TWEAKER_OT_tweaker_preset_add(AddPresetBase, bpy.types.Operator): 223 | ''' Add a new render preset''' 224 | bl_idname = "render.tweaker_preset_add" 225 | bl_label = "Add Tweaker Preset" 226 | bl_options = {'REGISTER', 'UNDO'} 227 | preset_menu = 'RENDER_TWEAKER_MT_tweaker_presets' 228 | preset_subdir = 'render_tweaker_presets' 229 | 230 | preset_defines = [ 231 | "render = bpy.context.scene.render", 232 | "cycles = bpy.context.scene.cycles" 233 | ] 234 | 235 | preset_values = [] 236 | for p in return_proplist(): 237 | pv = "cycles." + p 238 | preset_values.append(pv) 239 | 240 | 241 | # #################################################### 242 | # UI ################################################# 243 | # #################################################### 244 | 245 | 246 | class RENDER_TWEAKER_PT_main_ui(bpy.types.Panel): 247 | bl_idname = "render.render_tweaker" 248 | bl_label = "Render Tweaker" 249 | bl_space_type = 'PROPERTIES' 250 | bl_region_type = 'WINDOW' 251 | bl_context = "render" 252 | 253 | def draw(self, context): 254 | scene = context.scene 255 | layout = self.layout 256 | 257 | col = layout.column_flow(align=True) 258 | row = col.row(align=True) 259 | row.menu("RENDER_TWEAKER_MT_tweaker_presets", text=bpy.types.RENDER_TWEAKER_MT_tweaker_presets.bl_label) 260 | row.operator("render.tweaker_preset_add", text="", icon='ZOOMIN') 261 | row.operator("render.tweaker_preset_add", text="", icon='ZOOMOUT').remove_active=True 262 | 263 | row = layout.row(align=True) 264 | if not bpy.context.window_manager.recent_render == "": 265 | slot = bpy.context.window_manager.recent_render 266 | row.label(text="Recently stored: Slot %s" %slot) 267 | else: 268 | row.label(text="No slot stored in this session yet.") 269 | if scene.record_settings: 270 | row.operator("scene.enable_slot_recording", text="Slot Recording Enabled", icon="REC") 271 | else: 272 | row.operator("scene.enable_slot_recording", text="Slot Recording Disabled", icon="RADIOBUT_OFF") 273 | 274 | 275 | 276 | row = layout.row(align=True) 277 | row.operator("scene.save_main_rendersettings", text="Quick Save Settings") 278 | row.operator("scene.restore_main_rendersettings", text="Quick Restore Settings") 279 | 280 | 281 | 282 | class RENDER_TWEAKER_MT_tweaker_presets(bpy.types.Menu): 283 | bl_idname = "RENDER_TWEAKER_MT_tweaker_presets" 284 | bl_label = "Render Tweaker Presets" 285 | preset_subdir = "render_tweaker_presets" 286 | preset_operator = "script.execute_preset" 287 | 288 | draw = bpy.types.Menu.draw_preset 289 | 290 | 291 | # ################################################# 292 | # #### REGISTER ################################### 293 | # ################################################# 294 | 295 | 296 | classes = ( 297 | RENDER_TWEAKER_OT_enable_slot_recording, 298 | RENDER_TWEAKER_OT_render_slot_restore, 299 | RENDER_TWEAKER_OT_save_main_rendersettings, 300 | RENDER_TWEAKER_OT_restore_main_rendersettings, 301 | RENDER_TWEAKER_OT_tweaker_preset_add, 302 | RENDER_TWEAKER_MT_tweaker_presets, 303 | RENDER_TWEAKER_PT_main_ui 304 | ) 305 | 306 | def register(): 307 | for c in classes: 308 | register_class(c) 309 | 310 | bpy.app.handlers.render_complete.append(slot_handler) 311 | 312 | bpy.types.Scene.record_settings = BoolProperty( 313 | name = "Record Render Settings", 314 | description="After eacher render save the render settings in current render slot", 315 | default=False) 316 | bpy.types.Scene.master_scene = BoolProperty( 317 | name = "Master Scene", 318 | description="When working with multiple scenes, make this the master scene to copy settings from", 319 | default=False) 320 | bpy.types.WindowManager.recent_render = StringProperty( 321 | name = "Recently Rendered Slot", 322 | description = "Shows the most recently rendered slot", 323 | default="" 324 | ) 325 | 326 | wm = bpy.context.window_manager 327 | km = wm.keyconfigs.addon.keymaps.new(name='Image', space_type='IMAGE_EDITOR') 328 | kmi = km.keymap_items.new('scene.render_slot_restore', 'J', 'PRESS', ctrl=True) 329 | 330 | 331 | 332 | def unregister(): 333 | for c in classes: 334 | unregister_class(c) 335 | 336 | 337 | if slot_handler in bpy.app.handlers.render_complete: 338 | bpy.app.handlers.render_complete.remove(slot_handler) 339 | 340 | 341 | if __name__ == "__main__": 342 | register() 343 | -------------------------------------------------------------------------------- /setup_tracking_scene_new.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 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. 7 | # 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. 12 | # 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. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | # 20 | 21 | 22 | import bpy 23 | 24 | from bpy.types import Panel 25 | from bpy.app.translations import pgettext_iface as iface_ 26 | from bl_ui.utils import PresetPanel 27 | 28 | from bpy.types import Operator, PropertyGroup 29 | from bpy.props import FloatProperty, BoolProperty, PointerProperty 30 | from mathutils import ( 31 | Vector, 32 | Matrix, 33 | ) 34 | 35 | 36 | def CLIP_spaces_walk(context, all_screens, tarea, tspace, callback, *args): 37 | screens = bpy.data.screens if all_screens else [context.screen] 38 | 39 | for screen in screens: 40 | for area in screen.areas: 41 | if area.type == tarea: 42 | for space in area.spaces: 43 | if space.type == tspace: 44 | callback(space, *args) 45 | 46 | 47 | def CLIP_check_camera_has_distortion(tracking_camera): 48 | # TODO add new distortion_model 49 | if tracking_camera.distortion_model == 'POLYNOMIAL': 50 | return not all(k == 0 for k in (tracking_camera.k1, 51 | tracking_camera.k2, 52 | tracking_camera.k3)) 53 | elif tracking_camera.distortion_model == 'DIVISION': 54 | return not all(k == 0 for k in (tracking_camera.division_k1, 55 | tracking_camera.division_k2)) 56 | return False 57 | 58 | 59 | def CLIP_set_viewport_background(context, clip, clip_user): 60 | 61 | def set_background(cam, clip, user): 62 | bgpic = None 63 | 64 | for x in cam.background_images: 65 | if x.source == 'MOVIE_CLIP': 66 | bgpic = x 67 | break 68 | 69 | if not bgpic: 70 | bgpic = cam.background_images.new() 71 | 72 | bgpic.source = 'MOVIE_CLIP' 73 | bgpic.clip = clip 74 | bgpic.clip_user.proxy_render_size = user.proxy_render_size 75 | if CLIP_check_camera_has_distortion(clip.tracking.camera): 76 | bgpic.clip_user.use_render_undistorted = True 77 | bgpic.use_camera_clip = False 78 | 79 | cam.show_background_images = True 80 | 81 | scene_camera = context.scene.camera 82 | if (not scene_camera) or (scene_camera.type != 'CAMERA'): 83 | return 84 | set_background(scene_camera.data, clip, clip_user) 85 | 86 | 87 | def CLIP_camera_for_clip(context, clip): 88 | scene = context.scene 89 | camera = scene.camera 90 | 91 | for ob in scene.objects: 92 | if ob.type == 'CAMERA': 93 | for con in ob.constraints: 94 | if con.type == 'CAMERA_SOLVER': 95 | cur_clip = scene.active_clip if con.use_active_clip else con.clip 96 | 97 | if cur_clip == clip: 98 | return ob 99 | 100 | return camera 101 | 102 | 103 | def CLIP_findOrCreateCamera(context): 104 | scene = context.scene 105 | 106 | if scene.camera: 107 | return scene.camera 108 | 109 | cam = bpy.data.cameras.new(name="Camera") 110 | camob = bpy.data.objects.new(name="Camera", object_data=cam) 111 | scene.collection.objects.link(camob) 112 | 113 | scene.camera = camob 114 | 115 | camob.matrix_local = ( 116 | Matrix.Translation((0.0, -2.5, 1.6)) @ 117 | Matrix.Rotation(0.0, 4, 'Z') @ 118 | Matrix.Rotation(0.0, 4, 'Y') @ 119 | Matrix.Rotation(1.57079, 4, 'X') 120 | ) 121 | 122 | return camob 123 | 124 | def CLIP_setupCamera(context): 125 | sc = context.space_data 126 | clip = sc.clip 127 | tracking = clip.tracking 128 | 129 | camob = CLIP_findOrCreateCamera(context) 130 | cam = camob.data 131 | 132 | # Remove all constraints to be sure motion is fine. 133 | camob.constraints.clear() 134 | 135 | # Set the viewport background 136 | CLIP_set_viewport_background(context, sc.clip, sc.clip_user) 137 | 138 | # Append camera solver constraint. 139 | con = camob.constraints.new(type='CAMERA_SOLVER') 140 | con.use_active_clip = True 141 | con.influence = 1.0 142 | 143 | cam.sensor_width = tracking.camera.sensor_width 144 | cam.lens = tracking.camera.focal_length 145 | 146 | 147 | class CLIP_OT_new_setup_camera(Operator): 148 | bl_idname = "clip.new_setup_camera" 149 | bl_label = "Setup Camera" 150 | bl_options = {'UNDO', 'REGISTER'} 151 | 152 | @classmethod 153 | def poll(cls, context): 154 | sc = context.space_data 155 | return sc.type == 'CLIP_EDITOR' and sc.clip 156 | 157 | def execute(self, context): 158 | CLIP_findOrCreateCamera(context) 159 | CLIP_setupCamera(context) 160 | return {'FINISHED'} 161 | 162 | 163 | class CLIP_OT_new_set_active_clip(Operator): 164 | bl_label = "Set Active Clip" 165 | bl_idname = "clip.new_set_active_clip" 166 | 167 | @classmethod 168 | def poll(cls, context): 169 | sc = context.space_data 170 | return sc.type == 'CLIP_EDITOR' and sc.clip 171 | 172 | def execute(self, context): 173 | clip = context.space_data.clip 174 | scene = context.scene 175 | scene.active_clip = clip 176 | scene.render.resolution_x = clip.size[0] 177 | scene.render.resolution_y = clip.size[1] 178 | return {'FINISHED'} 179 | 180 | 181 | class CLIP_OT_create_tracking_object(Operator): 182 | """Create an Empty Object in 3D viewport at position of active track""" 183 | bl_idname = "clip.create_tracking_object" 184 | bl_label = "Create Tracking Object" 185 | 186 | @classmethod 187 | def poll(cls, context): 188 | sc = context.space_data 189 | solved_tracking_object = False 190 | if sc.type == 'CLIP_EDITOR' and sc.clip: 191 | tracking_object = sc.clip.tracking.objects.active 192 | if not tracking_object.is_camera: 193 | if tracking_object.reconstruction.is_valid: 194 | solved_tracking_object = True 195 | 196 | return solved_tracking_object 197 | 198 | def execute(self, context): 199 | scene = context.scene 200 | sc = context.space_data 201 | clip = sc.clip 202 | collections = bpy.data.collections 203 | 204 | tracking_object = sc.clip.tracking.objects.active 205 | active_track = tracking_object.tracks.active 206 | empty = bpy.data.objects.new(f'{tracking_object.name}_{active_track.name}', None) 207 | empty.empty_display_size = sc.clip.tracking.settings.object_distance/2 208 | empty.empty_display_type = 'SPHERE' 209 | if collections.get("foreground"): 210 | collections["foreground"].objects.link(empty) 211 | else: 212 | collections[0].objects.link(empty) 213 | 214 | con = empty.constraints.new(type='OBJECT_SOLVER') 215 | con.use_active_clip = True 216 | con.camera = context.scene.camera 217 | con.object = tracking_object.name 218 | con.influence = 1.0 219 | context.view_layer.objects.active = empty 220 | bpy.ops.constraint.objectsolver_set_inverse(constraint="Object Solver", owner='OBJECT') 221 | 222 | # get position 223 | matrix = Matrix.Identity(4) 224 | reconstruction = tracking_object.reconstruction 225 | framenr = scene.frame_current - clip.frame_start + 1 226 | reconstructed_matrix = reconstruction.cameras.matrix_from_frame(frame=framenr) 227 | matrix = scene.camera.matrix_world @ reconstructed_matrix.inverted() 228 | bundle = active_track.bundle 229 | 230 | empty.matrix_world.translation = matrix @ bundle 231 | 232 | return {'FINISHED'} 233 | 234 | 235 | class CLIP_OT_new_set_viewport_background(Operator): 236 | """Set current movie clip as a camera background in 3D Viewport """ \ 237 | """(works only when a 3D Viewport is visible)""" 238 | 239 | bl_idname = "clip.new_set_viewport_background" 240 | bl_label = "Set as Background" 241 | bl_options = {'REGISTER'} 242 | 243 | @classmethod 244 | def poll(cls, context): 245 | if context.space_data.type != 'CLIP_EDITOR': 246 | return False 247 | 248 | sc = context.space_data 249 | 250 | return sc.clip 251 | 252 | def execute(self, context): 253 | sc = context.space_data 254 | CLIP_set_viewport_background(context, sc.clip, sc.clip_user) 255 | 256 | return {'FINISHED'} 257 | 258 | 259 | class CLIP_OT_new_setup_tracking_scene(Operator): 260 | """Prepare scene for compositing 3D objects into this footage""" 261 | # TODO: it will be great to integrate with other engines (other than Cycles) 262 | 263 | bl_idname = "clip.new_setup_tracking_scene" 264 | bl_label = "Setup Tracking Scene" 265 | bl_options = {'UNDO', 'REGISTER'} 266 | 267 | 268 | @classmethod 269 | def poll(cls, context): 270 | sc = context.space_data 271 | 272 | if sc.type != 'CLIP_EDITOR': 273 | return False 274 | 275 | clip = sc.clip 276 | 277 | return clip and clip.tracking.reconstruction.is_valid 278 | 279 | @staticmethod 280 | def _setupScene(context): 281 | scene = context.scene 282 | scene.active_clip = context.space_data.clip 283 | scene.render.use_motion_blur = True 284 | scene.render.film_transparent = True 285 | 286 | 287 | @staticmethod 288 | def _setupViewport(context): 289 | sc = context.space_data 290 | CLIP_set_viewport_background(context, sc.clip, sc.clip_user) 291 | 292 | 293 | @staticmethod 294 | def createCollection(context, collection_name): 295 | def collection_in_collection(collection, collection_to_query): 296 | """Return true if collection is in any of the children or """ 297 | """grandchildren of collection_to_query""" 298 | for child in collection_to_query.children: 299 | if collection == child: 300 | return True 301 | 302 | if collection_in_collection(collection, child): 303 | return True 304 | 305 | master_collection = context.scene.collection 306 | collection = bpy.data.collections.get(collection_name) 307 | 308 | if collection and collection.library: 309 | # We need a local collection instead. 310 | collection = None 311 | 312 | if not collection: 313 | collection = bpy.data.collections.new(name=collection_name) 314 | master_collection.children.link(collection) 315 | else: 316 | # see if collection is in the scene 317 | if not collection_in_collection(collection, master_collection): 318 | master_collection.children.link(collection) 319 | 320 | def _setupCollections(self, context): 321 | def setup_collection_recursively(collections, collection_name, attr_name): 322 | for collection in collections: 323 | if collection.collection.name == collection_name: 324 | setattr(collection, attr_name, True) 325 | break 326 | else: 327 | setup_collection_recursively(collection.children, collection_name, attr_name) 328 | 329 | collections = context.scene.collection.children 330 | 331 | # rename base collection to foreground or create it 332 | if len(collections) == 1: 333 | collections[0].name = "foreground" 334 | else: 335 | self.createCollection(context, "foreground") 336 | if context.scene.use_shadow_catcher: 337 | self.createCollection(context, "shadow catcher") 338 | 339 | 340 | @staticmethod 341 | def _wipeDefaultNodes(tree): 342 | if len(tree.nodes) != 2: 343 | return False 344 | types = [node.type for node in tree.nodes] 345 | types.sort() 346 | 347 | if types[0] == 'COMPOSITE' and types[1] == 'R_LAYERS': 348 | while tree.nodes: 349 | tree.nodes.remove(tree.nodes[0]) 350 | 351 | @staticmethod 352 | def _findNode(tree, type): 353 | for node in tree.nodes: 354 | if node.bl_idname == type: 355 | return node 356 | 357 | return None 358 | 359 | @staticmethod 360 | def _findOrCreateNode(tree, type): 361 | node = CLIP_OT_new_setup_tracking_scene._findNode(tree, type) 362 | 363 | if not node: 364 | node = tree.nodes.new(type=type) 365 | print("did not find", node) 366 | 367 | return node 368 | 369 | @staticmethod 370 | def _needSetupNodes(context): 371 | scene = context.scene 372 | tree = scene.node_tree 373 | 374 | if not tree: 375 | # No compositor node tree found, time to create it! 376 | return True 377 | 378 | for node in tree.nodes: 379 | if node.type in {'MOVIECLIP', 'MOVIEDISTORTION'}: 380 | return False 381 | 382 | return True 383 | 384 | @staticmethod 385 | def _offsetNodes(tree): 386 | for a in tree.nodes: 387 | for b in tree.nodes: 388 | if a != b and a.location == b.location: 389 | b.location += Vector((40.0, 20.0)) 390 | 391 | 392 | def _setup_shadow_catcher(self, context, material): 393 | tree = material.node_tree 394 | 395 | principled = self._findOrCreateNode(tree, 'ShaderNodeBsdfPrincipled') 396 | output = self._findOrCreateNode(tree, 'ShaderNodeOutputMaterial') 397 | 398 | tree.links.new(principled.outputs[0], output.inputs[0]) 399 | 400 | output.location = principled.location 401 | output.location += Vector((300.0, 0.0)) 402 | 403 | if context.scene.render.engine == 'BLENDER_EEVEE': 404 | diffuse = self._findOrCreateNode(tree, 'ShaderNodeBsdfDiffuse') 405 | shader_to_rgb = self._findOrCreateNode(tree, 'ShaderNodeShaderToRGB') 406 | maprange = self._findOrCreateNode(tree, 'ShaderNodeMapRange') 407 | 408 | tree.links.new(diffuse.outputs[0], shader_to_rgb.inputs[0]) 409 | tree.links.new(shader_to_rgb.outputs[0], maprange.inputs[0]) 410 | tree.links.new(maprange.outputs[0], principled.inputs['Alpha']) 411 | 412 | 413 | maprange.location = principled.location 414 | maprange.location += Vector((-300.0, -300.0)) 415 | 416 | shader_to_rgb.location = maprange.location 417 | shader_to_rgb.location += Vector((-200.0, 0.0)) 418 | 419 | diffuse.location = shader_to_rgb.location 420 | diffuse.location += Vector((-200.0, 0.0)) 421 | 422 | maprange.interpolation_type = 'SMOOTHSTEP' 423 | maprange.inputs[1].default_value = 0.3 424 | maprange.inputs[2].default_value = 0 425 | 426 | principled.inputs['Specular'].default_value = 0.0 427 | principled.inputs['Base Color'].default_value = (0.1, 0.1, 0.1, 1.0) 428 | 429 | material.blend_method = 'BLEND' 430 | 431 | 432 | def _setupNodes(self, context): 433 | if not self._needSetupNodes(context): 434 | # Compositor nodes were already setup or even changes already 435 | # do nothing to prevent nodes damage. 436 | return 437 | 438 | # Enable backdrop for all compositor spaces. 439 | def setup_space(space): 440 | space.show_backdrop = True 441 | 442 | CLIP_spaces_walk(context, True, 'NODE_EDITOR', 'NODE_EDITOR', 443 | setup_space) 444 | 445 | sc = context.space_data 446 | scene = context.scene 447 | scene.use_nodes = True 448 | tree = scene.node_tree 449 | clip = sc.clip 450 | 451 | need_undistortion = CLIP_check_camera_has_distortion(clip.tracking.camera) 452 | 453 | # Remove all the nodes if they came from default node setup. 454 | # This is simplest way to make it so final node setup is correct. 455 | self._wipeDefaultNodes(tree) 456 | 457 | # Create nodes. 458 | rlayer = self._findOrCreateNode(tree, 'CompositorNodeRLayers') 459 | composite = self._findOrCreateNode(tree, 'CompositorNodeComposite') 460 | 461 | movieclip = tree.nodes.new(type='CompositorNodeMovieClip') 462 | 463 | if need_undistortion: 464 | distortion = tree.nodes.new(type='CompositorNodeMovieDistortion') 465 | 466 | 467 | scale = tree.nodes.new(type='CompositorNodeScale') 468 | alphaover = tree.nodes.new(type='CompositorNodeAlphaOver') 469 | viewer = tree.nodes.new(type='CompositorNodeViewer') 470 | 471 | # Setup nodes. 472 | movieclip.clip = clip 473 | 474 | if need_undistortion: 475 | distortion.clip = clip 476 | distortion.distortion_type = 'UNDISTORT' 477 | 478 | scale.space = 'RENDER_SIZE' 479 | 480 | rlayer.scene = scene 481 | rlayer.layer = "View Layer" 482 | 483 | # Create links. 484 | if need_undistortion: 485 | tree.links.new(movieclip.outputs["Image"], distortion.inputs["Image"]) 486 | tree.links.new(distortion.outputs["Image"], scale.inputs["Image"]) 487 | else: 488 | tree.links.new(movieclip.outputs["Image"], scale.inputs["Image"]) 489 | 490 | 491 | tree.links.new(scale.outputs["Image"], alphaover.inputs[1]) 492 | 493 | tree.links.new(rlayer.outputs["Image"], alphaover.inputs[2]) 494 | 495 | tree.links.new(alphaover.outputs["Image"], composite.inputs["Image"]) 496 | tree.links.new(alphaover.outputs["Image"], viewer.inputs["Image"]) 497 | 498 | # Place nodes. 499 | movieclip.location = Vector((-300.0, 350.0)) 500 | 501 | if need_undistortion: 502 | distortion.location = movieclip.location 503 | distortion.location += Vector((200.0, 0.0)) 504 | scale.location = distortion.location 505 | else: 506 | scale.location = movieclip.location 507 | scale.location += Vector((200.0, 0.0)) 508 | 509 | alphaover.location = scale.location 510 | alphaover.location += Vector((250.0, -250.0)) 511 | 512 | composite.location = alphaover.location 513 | composite.location += Vector((300.0, -100.0)) 514 | 515 | viewer.location = composite.location 516 | composite.location += Vector((0.0, 200.0)) 517 | 518 | rlayer.location = movieclip.location 519 | rlayer.location += Vector((0.0, -375.0)) 520 | 521 | # Ensure no nodes were created on the position of existing node. 522 | self._offsetNodes(tree) 523 | 524 | @staticmethod 525 | def _createMesh(collection, name, vertices, faces): 526 | from bpy_extras.io_utils import unpack_list 527 | 528 | mesh = bpy.data.meshes.new(name=name) 529 | 530 | mesh.vertices.add(len(vertices)) 531 | mesh.vertices.foreach_set("co", unpack_list(vertices)) 532 | 533 | nbr_loops = len(faces) 534 | nbr_polys = nbr_loops // 4 535 | mesh.loops.add(nbr_loops) 536 | mesh.polygons.add(nbr_polys) 537 | 538 | mesh.polygons.foreach_set("loop_start", range(0, nbr_loops, 4)) 539 | mesh.polygons.foreach_set("loop_total", (4,) * nbr_polys) 540 | mesh.loops.foreach_set("vertex_index", faces) 541 | 542 | mesh.update() 543 | 544 | ob = bpy.data.objects.new(name=name, object_data=mesh) 545 | collection.objects.link(ob) 546 | 547 | return ob 548 | 549 | @staticmethod 550 | def _getPlaneVertices(half_size, z): 551 | 552 | return [(-half_size, -half_size, z), 553 | (half_size, -half_size, z), 554 | (half_size, half_size, z), 555 | (-half_size, half_size, z)] 556 | 557 | def _createGround(self, collection): 558 | vertices = self._getPlaneVertices(4.0, 0.0) 559 | faces = [0, 1, 2, 3] 560 | 561 | ob = self._createMesh(collection, "Ground", vertices, faces) 562 | ob["is_ground"] = True 563 | 564 | return ob 565 | 566 | @staticmethod 567 | def _findGround(context): 568 | scene = context.scene 569 | 570 | for ob in scene.objects: 571 | if ob.type == 'MESH' and "is_ground" in ob: 572 | return ob 573 | 574 | return None 575 | 576 | @staticmethod 577 | def _createLight(): 578 | light = bpy.data.lights.new(name="Light", type='POINT') 579 | lightob = bpy.data.objects.new(name="Light", object_data=light) 580 | 581 | lightob.matrix_local = Matrix.Translation((4.076, 1.005, 5.904)) 582 | 583 | return lightob 584 | 585 | def _createSampleObject(self, collection): 586 | vertices = self._getPlaneVertices(1.0, -1.0) + \ 587 | self._getPlaneVertices(1.0, 1.0) 588 | faces = (0, 1, 2, 3, 589 | 4, 7, 6, 5, 590 | 0, 4, 5, 1, 591 | 1, 5, 6, 2, 592 | 2, 6, 7, 3, 593 | 3, 7, 4, 0) 594 | 595 | return self._createMesh(collection, "Cube", vertices, faces) 596 | 597 | def _setupObjects(self, context): 598 | 599 | def setup_shadow_catcher_objects(collection): 600 | """Make all the newly created and the old objects of a collection """ \ 601 | """to be properly setup for shadow catch""" 602 | for ob in collection.objects: 603 | # assign or create correct shadowcatcher based on render engine 604 | engine = context.scene.render.engine 605 | engine_name = "" 606 | if engine == "BLENDER_EEVEE": 607 | engine_name = "eevee" 608 | elif engine == 'CYCLES': 609 | engine_name = "cycles" 610 | shadowcatchername = f'shadowcatcher_{engine_name}' 611 | if not bpy.data.materials.get(shadowcatchername): 612 | shadowcatcher_mat = bpy.data.materials.new(name=shadowcatchername) 613 | else: 614 | shadowcatcher_mat = bpy.data.materials[shadowcatchername] 615 | shadowcatcher_mat.use_nodes = True 616 | self._setup_shadow_catcher(context, shadowcatcher_mat) 617 | if not bool(ob.data.materials): 618 | ob.data.materials.append(shadowcatcher_mat) 619 | else: 620 | ob.data.materials[0] = shadowcatcher_mat 621 | if scene.render.engine == 'CYCLES': 622 | ob.cycles.is_shadow_catcher = True 623 | # put objects in collection 624 | for child in collection.children: 625 | setup_shadow_catcher_collection(child) 626 | 627 | scene = context.scene 628 | fg_coll = bpy.data.collections["foreground", None] 629 | 630 | # Ensure all lights are active on foreground and background. 631 | has_light = False 632 | has_mesh = False 633 | for ob in scene.objects: 634 | if ob.type == 'LIGHT': 635 | has_light = True 636 | elif ob.type == 'MESH' and "is_ground" not in ob: 637 | has_mesh = True 638 | 639 | # Create sample light if there is no lights in the scene. 640 | if not has_light: 641 | light = self._createLight() 642 | context.scene.collection.objects.link(light) 643 | # fg_coll.objects.link(light) 644 | # bg_coll.objects.link(light) 645 | 646 | # Create sample object if there's no meshes in the scene. 647 | if not has_mesh: 648 | ob = self._createSampleObject(fg_coll) 649 | 650 | # Create ground object if needed. 651 | if scene.use_shadow_catcher: 652 | bg_coll = bpy.data.collections["shadow catcher", None] 653 | ground = self._findGround(context) 654 | if not ground: 655 | ground = self._createGround(bg_coll) 656 | 657 | # And set everything on background layer to shadow catcher. 658 | setup_shadow_catcher_objects(bg_coll) 659 | 660 | def execute(self, context): 661 | self._setupScene(context) 662 | self._setupViewport(context) 663 | CLIP_findOrCreateCamera(context) 664 | CLIP_setupCamera(context) 665 | self._setupCollections(context) 666 | if context.scene.create_node_tree: 667 | self._setupNodes(context) 668 | self._setupObjects(context) 669 | 670 | return {'FINISHED'} 671 | 672 | 673 | class CLIP_PT_new_tools_scenesetup(Panel): 674 | bl_space_type = 'CLIP_EDITOR' 675 | bl_region_type = 'TOOLS' 676 | bl_label = "New Scene Setup" 677 | bl_translation_context = bpy.app.translations.contexts.id_movieclip 678 | bl_category = "Solve" 679 | 680 | @classmethod 681 | def poll(cls, context): 682 | sc = context.space_data 683 | clip = sc.clip 684 | 685 | return clip and sc.view == 'CLIP' and sc.mode != 'MASK' 686 | 687 | def draw(self, _context): 688 | clip = _context.space_data.clip 689 | tracking = clip.tracking 690 | layout = self.layout 691 | layout.use_property_split = True 692 | layout.use_property_decorate = False 693 | scene = _context.scene 694 | 695 | col = layout.column(align=True) 696 | col.prop(scene.render, "engine", text="Engine") 697 | col = layout.column(heading="Setup", align=True) 698 | col.prop(_context.scene, "create_node_tree") 699 | row = col.row() 700 | row.active = not 'WORKBENCH' in scene.render.engine 701 | row.prop(_context.scene, "use_shadow_catcher") 702 | col = layout.column() 703 | col.operator("clip.new_set_viewport_background") 704 | col.operator("clip.new_setup_camera") 705 | col.operator("clip.new_setup_tracking_scene") 706 | col = layout.column() 707 | col.active = not clip.tracking.objects.active.is_camera 708 | col.operator("clip.create_tracking_object", text="Setup Object") 709 | 710 | 711 | classes = ( 712 | CLIP_OT_new_setup_camera, 713 | CLIP_OT_new_set_active_clip, 714 | CLIP_OT_new_set_viewport_background, 715 | CLIP_OT_new_setup_tracking_scene, 716 | CLIP_OT_create_tracking_object, 717 | CLIP_PT_new_tools_scenesetup 718 | ) 719 | # Register everything 720 | def register(): 721 | for cls in classes: 722 | bpy.utils.register_class(cls) 723 | 724 | bpy.types.Scene.use_shadow_catcher = bpy.props.BoolProperty( 725 | name="Shadow Catcher", 726 | description="Create a shadow catcher object", 727 | default=False, 728 | ) 729 | bpy.types.Scene.create_node_tree = bpy.props.BoolProperty( 730 | name="Nodes", 731 | description="Generate a node tree for compositing", 732 | default=False, 733 | ) 734 | 735 | def unregister(): 736 | for cls in classes: 737 | bpy.utils.unregister_class(cls) 738 | 739 | if __name__ == '__main__': 740 | register() 741 | -------------------------------------------------------------------------------- /snapping_pies.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Snapping Pies", 3 | "author": "Sebastian Koenig, Ivan Santic", 4 | "version": (0, 3), 5 | "blender": (2, 8, 0), 6 | "description": "Custom Pie Menus", 7 | "category": "3D View",} 8 | 9 | 10 | 11 | import bpy 12 | from bpy.types import Menu, Operator, Panel 13 | 14 | 15 | ########### CUSTOM OPERATORS ############### 16 | 17 | 18 | class VIEW3D_OT_toggle_pivot(Operator): 19 | """Toggle between 3D-Cursor and Median pivoting""" 20 | bl_idname = "scene.toggle_pivot" 21 | bl_label = "Toggle Pivot" 22 | 23 | @classmethod 24 | def poll(cls, context): 25 | space = context.space_data 26 | return space.type == 'VIEW_3D' 27 | 28 | def execute(self, context): 29 | pivot = context.space_data.pivot_point 30 | if pivot == "CURSOR": 31 | context.space_data.pivot_point = "MEDIAN_POINT" 32 | elif pivot == "INDIVIDUAL_ORIGINS": 33 | context.space_data.pivot_point = "MEDIAN_POINT" 34 | else: 35 | context.space_data.pivot_point = "CURSOR" 36 | return {'FINISHED'} 37 | 38 | 39 | 40 | class VIEW3D_OT_object_to_marker(Operator): 41 | "Set the object's origin to the mesh selection and place the object to the active 3d Marker" 42 | bl_idname = "object.object_to_marker" 43 | bl_label = "Snap Origin to 3D Marker" 44 | 45 | @classmethod 46 | def poll(cls, context): 47 | sc = context.space_data 48 | clip = context.scene.active_clip 49 | average_error = clip.tracking.objects.active.reconstruction.average_error 50 | active_object = context.view_layer.objects.active 51 | return (sc.type == 'VIEW_3D' and average_error > 0.0 and active_object != context.scene.camera) 52 | 53 | def execute(self, context): 54 | cursor_location = context.scene.cursor.location.copy() 55 | active_object = context.view_layer.objects.active 56 | active_camera = context.scene.camera 57 | # make sure only the camera is selected 58 | bpy.ops.object.select_all(action='DESELECT') 59 | context.view_layer.objects.active = active_camera 60 | active_camera.select_set(True) 61 | bpy.ops.view3d.snap_cursor_to_selected() 62 | marker_location = context.scene.cursor.location.copy() 63 | active_camera.select_set(False) 64 | 65 | context.view_layer.objects.active = active_object 66 | active_object.select_set(True) 67 | active_object.location = marker_location 68 | context.scene.cursor.location = cursor_location 69 | 70 | return {'FINISHED'} 71 | 72 | 73 | class VIEW3D_OT_origin_to_marker(Operator): 74 | "Set the object's origin to the mesh selection and place the object to the active 3d Marker" 75 | bl_idname = "object.origin_to_marker" 76 | bl_label = "Snap Origin to 3D Marker" 77 | 78 | @classmethod 79 | def poll(cls, context): 80 | sc = context.space_data 81 | clip = context.scene.active_clip 82 | average_error = clip.tracking.objects.active.reconstruction.average_error 83 | return (sc.type == 'VIEW_3D' and context.object.mode == 'EDIT' and average_error > 0.0) 84 | 85 | def execute(self, context): 86 | cursor_location = context.scene.cursor.location.copy() 87 | 88 | # set origin to selected vertex 89 | bpy.ops.view3d.snap_cursor_to_selected() 90 | bpy.ops.object.mode_set(mode="OBJECT") 91 | bpy.ops.object.origin_set(type='ORIGIN_CURSOR') 92 | 93 | # store the location of the 3d marker 94 | active_object = context.view_layer.objects.active 95 | active_camera = context.scene.camera 96 | # make sure only the camera is selected 97 | bpy.ops.object.select_all(action='DESELECT') 98 | context.view_layer.objects.active = active_camera 99 | active_camera.select_set(True) 100 | bpy.ops.view3d.snap_cursor_to_selected() 101 | marker_location = context.scene.cursor.location.copy() 102 | active_camera.select_set(False) 103 | 104 | context.view_layer.objects.active = active_object 105 | active_object.select_set(True) 106 | active_object.location = marker_location 107 | context.scene.cursor.location = cursor_location 108 | return {'FINISHED'} 109 | 110 | class VIEW3D_OT_origin_to_selected(Operator): 111 | "Set the object's origin to the 3d cursor. Works only in edit mode" 112 | bl_idname = "object.origin_to_selected" 113 | bl_label = "Origin to Selection" 114 | 115 | @classmethod 116 | def poll(cls, context): 117 | sc = context.space_data 118 | return (sc.type == 'VIEW_3D' and context.object.mode == 'EDIT') 119 | 120 | def execute(self, context): 121 | cursor_location = context.scene.cursor.location.copy() 122 | bpy.ops.view3d.snap_cursor_to_selected() 123 | bpy.ops.object.mode_set(mode="OBJECT") 124 | bpy.ops.object.origin_set(type='ORIGIN_CURSOR') 125 | context.scene.cursor.location = cursor_location 126 | return {'FINISHED'} 127 | 128 | 129 | class VIEW3D_OT_origin_to_geometry(Operator): 130 | bl_idname="object.origin_to_geometry" 131 | bl_label="Origin to Geometry" 132 | 133 | @classmethod 134 | def poll(cls, context): 135 | sc = context.space_data 136 | return (sc.type == 'VIEW_3D' and context.object.mode == 'EDIT') 137 | 138 | def execute(self, context): 139 | bpy.ops.object.mode_set(mode="OBJECT") 140 | bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') 141 | bpy.ops.object.mode_set(mode="EDIT") 142 | return {'FINISHED'} 143 | 144 | 145 | 146 | 147 | 148 | #Menu Snap Element 149 | class VIEW3D_PIE_SnapElementMenu(Menu): 150 | bl_label = "Snap Element" 151 | 152 | def draw(self, context): 153 | settings = bpy.context.scene.tool_settings 154 | layout = self.layout 155 | pie = layout.menu_pie() 156 | pie.prop(settings, "snap_element", expand=True) 157 | 158 | 159 | #Menu Snap Element 160 | class VIEW3D_OT_SetPivotIndividual(Operator): 161 | bl_label = "Individual Origins" 162 | bl_idname = "object.setpivotindidual" 163 | 164 | @classmethod 165 | def poll(cls, context): 166 | sc = context.space_data 167 | return (sc.type == 'VIEW_3D') 168 | 169 | def execute(self, context): 170 | bpy.context.space_data.pivot_point = "INDIVIDUAL_ORIGINS" 171 | return {'FINISHED'} 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | ################### PIES ##################### 180 | 181 | class VIEW3D_PIE_MT_snapping_pie(Menu): 182 | # label is displayed at the center of the pie menu. 183 | bl_label = "Snapping and Origin" 184 | bl_idname = "VIEW3D_PIE_MT_snapping_pie" 185 | 186 | def draw(self, context): 187 | context = bpy.context 188 | layout = self.layout 189 | tool_settings = context.scene.tool_settings 190 | 191 | clip = context.scene.active_clip 192 | tracks = getattr(getattr(clip, "tracking", None), "tracks", None) 193 | track_active = tracks.active if tracks else None 194 | pie = layout.menu_pie() 195 | editmode = context.object.mode == 'EDIT' 196 | 197 | # set origin to selection in Edit Mode, set origin to cursor in Object mode 198 | if context.active_object: 199 | # Origin to Geometry 200 | if editmode: 201 | pie.operator("object.origin_set", text="Origin to Geometry").type="ORIGIN_GEOMETRY" 202 | else: 203 | pie.operator("object.origin_to_geometry") 204 | 205 | # Origin to Cursor / Selected 206 | if editmode: 207 | pie.operator("object.origin_to_selected") 208 | else: 209 | pie.operator("object.origin_set", text="Origin to Cursor").type="ORIGIN_CURSOR" 210 | 211 | pie.operator("view3d.snap_cursor_to_selected", icon="CURSOR") 212 | pie.operator( 213 | "view3d.snap_selected_to_cursor", 214 | text="Selection to Cursor", 215 | icon='RESTRICT_SELECT_OFF', 216 | ).use_offset = False 217 | 218 | pie.operator("view3d.snap_cursor_to_center", icon="CURSOR") 219 | if track_active: 220 | if editmode: 221 | pie.operator("object.origin_to_marker") 222 | else: 223 | pie.operator("object.object_to_marker") 224 | 225 | pie.operator("scene.toggle_pivot") 226 | 227 | 228 | 229 | ########## REGISTER ############ 230 | classes = { 231 | VIEW3D_OT_toggle_pivot, 232 | VIEW3D_OT_origin_to_selected, 233 | VIEW3D_OT_origin_to_geometry, 234 | VIEW3D_OT_SetPivotIndividual, 235 | VIEW3D_OT_origin_to_marker, 236 | VIEW3D_OT_object_to_marker, 237 | VIEW3D_PIE_MT_snapping_pie, 238 | } 239 | 240 | 241 | def register(): 242 | for c in classes: 243 | bpy.utils.register_class(c) 244 | 245 | wm = bpy.context.window_manager 246 | 247 | km = wm.keyconfigs.addon.keymaps.new(name = 'Object Mode') 248 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS', shift=True).properties.name = "VIEW3D_PIE_MT_snapping_pie" 249 | 250 | km = wm.keyconfigs.addon.keymaps.new(name = 'Mesh') 251 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS',shift=True).properties.name = "VIEW3D_PIE_MT_snapping_pie" 252 | 253 | km = wm.keyconfigs.addon.keymaps.new(name = 'Curve') 254 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS',shift=True).properties.name = "VIEW3D_PIE_MT_snapping_pie" 255 | 256 | km = wm.keyconfigs.addon.keymaps.new(name = 'Armature') 257 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS',shift=True).properties.name = "VIEW3D_PIE_MT_snapping_pie" 258 | 259 | km = wm.keyconfigs.addon.keymaps.new(name = 'Pose') 260 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS',shift=True).properties.name = "VIEW3D_PIE_MT_snapping_pie" 261 | 262 | 263 | 264 | def unregister(): 265 | for c in classes: 266 | bpy.utils.unregister_class(c) 267 | 268 | if __name__ == "__main__": 269 | register() 270 | #bpy.ops.wm.call_menu_pie(name="mesh.mesh_operators") 271 | -------------------------------------------------------------------------------- /snapping_pies_2.79.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Snapping Pies", 3 | "author": "Sebastian Koenig, Ivan Santic", 4 | "version": (0, 2), 5 | "blender": (2, 7, 2), 6 | "description": "Custom Pie Menus", 7 | "category": "3D View",} 8 | 9 | 10 | 11 | import bpy 12 | from bpy.types import Menu 13 | 14 | 15 | ###### FUNTIONS ########## 16 | 17 | def origin_to_selection(context): 18 | context = bpy.context 19 | 20 | if context.object.mode == "EDIT": 21 | saved_location = context.scene.cursor_location.copy() 22 | bpy.ops.view3d.snap_cursor_to_selected() 23 | bpy.ops.object.mode_set(mode="OBJECT") 24 | bpy.ops.object.origin_set(type='ORIGIN_CURSOR') 25 | context.scene.cursor_location = saved_location 26 | 27 | 28 | def origin_to_geometry(context): 29 | context = bpy.context 30 | 31 | if context.object.mode == "EDIT": 32 | bpy.ops.object.mode_set(mode="OBJECT") 33 | bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') 34 | bpy.ops.object.mode_set(mode="EDIT") 35 | 36 | 37 | 38 | 39 | ########### CUSTOM OPERATORS ############### 40 | 41 | 42 | class VIEW3D_PIE_Snapping_Extras(Menu): 43 | bl_label = "Snapping Extras" 44 | 45 | 46 | def draw(self, context): 47 | layout = self.layout 48 | space = context.space_data 49 | 50 | pie = layout.menu_pie() 51 | pie.operator("view3d.snap_cursor_to_center", icon="MANIPUL") 52 | pie.operator("view3d.snap_cursor_to_active", icon="CURSOR") 53 | pie.operator("view3d.snap_selected_to_grid", icon="GRID") 54 | pie.operator("view3d.snap_cursor_to_grid", icon="GRID") 55 | 56 | 57 | 58 | class VIEW3D_OT_toggle_pivot(bpy.types.Operator): 59 | """Toggle between 3D-Cursor and Median pivoting""" 60 | bl_idname = "scene.toggle_pivot" 61 | bl_label = "Toggle Pivot" 62 | 63 | @classmethod 64 | def poll(cls, context): 65 | space = context.space_data 66 | return space.type == 'VIEW_3D' 67 | 68 | def execute(self, context): 69 | pivot = context.space_data.pivot_point 70 | if pivot == "CURSOR": 71 | context.space_data.pivot_point = "MEDIAN_POINT" 72 | elif pivot == "INDIVIDUAL_ORIGINS": 73 | context.space_data.pivot_point = "MEDIAN_POINT" 74 | else: 75 | context.space_data.pivot_point = "CURSOR" 76 | return {'FINISHED'} 77 | 78 | 79 | 80 | class VIEW3D_OT_origin_to_selected(bpy.types.Operator): 81 | bl_idname="object.origin_to_selected" 82 | bl_label="Origin to Selection" 83 | 84 | @classmethod 85 | def poll(cls, context): 86 | sc = context.space_data 87 | return (sc.type == 'VIEW_3D') 88 | 89 | def execute(self, context): 90 | origin_to_selection(context) 91 | return {'FINISHED'} 92 | 93 | 94 | class VIEW3D_OT_origin_to_geometry(bpy.types.Operator): 95 | bl_idname="object.origin_to_geometry" 96 | bl_label="Origin to Geometry" 97 | 98 | @classmethod 99 | def poll(cls, context): 100 | sc = context.space_data 101 | return (sc.type == 'VIEW_3D') 102 | 103 | def execute(self, context): 104 | origin_to_geometry(context) 105 | return {'FINISHED'} 106 | 107 | 108 | #Menu Snap Target 109 | class VIEW3D_PIE_SnapTarget(Menu): 110 | bl_label = "Snap Target Menu" 111 | 112 | def draw(self, context): 113 | layout = self.layout 114 | pie = layout.menu_pie() 115 | 116 | pie.operator("object.snaptargetvariable", text="Active", icon="SNAP_SURFACE").variable='ACTIVE' 117 | pie.operator("object.snaptargetvariable", text="Median", icon="SNAP_SURFACE").variable='MEDIAN' 118 | pie.operator("object.snaptargetvariable", text="Center", icon="SNAP_SURFACE").variable='CENTER' 119 | pie.operator("object.snaptargetvariable", text="Closest", icon="SNAP_SURFACE").variable='CLOSEST' 120 | pie.operator("object.snapelementvariable", text="Face", icon="SNAP_FACE").variable='FACE' 121 | pie.operator("object.snapelementvariable", text="Vertex", icon="SNAP_VERTEX").variable='VERTEX' 122 | pie.operator("object.snapelementvariable", text="Edge", icon="SNAP_EDGE").variable='EDGE' 123 | pie.operator("object.snapelementvariable", text="Increment", icon="SNAP_INCREMENT").variable='INCREMENT' 124 | 125 | 126 | 127 | class VIEW3D_OT_SnapTargetVariable(bpy.types.Operator): 128 | bl_idname = "object.snaptargetvariable" 129 | bl_label = "Snap Target Variable" 130 | variable = bpy.props.StringProperty() 131 | 132 | @classmethod 133 | def poll(cls, context): 134 | return True 135 | 136 | def execute(self, context): 137 | bpy.context.scene.tool_settings.snap_target=self.variable 138 | return {'FINISHED'} 139 | 140 | 141 | 142 | class VIEW3D_OT_SnapElementVariable(bpy.types.Operator): 143 | bl_idname = "object.snapelementvariable" 144 | bl_label = "Snap Element Variable" 145 | variable = bpy.props.StringProperty() 146 | 147 | @classmethod 148 | def poll(cls, context): 149 | return True 150 | 151 | def execute(self, context): 152 | bpy.context.scene.tool_settings.snap_element=self.variable 153 | return {'FINISHED'} 154 | 155 | 156 | 157 | #Menu Snap Element 158 | class VIEW3D_PIE_SnapElementMenu(Menu): 159 | bl_label = "Snap Element" 160 | 161 | def draw(self, context): 162 | settings = bpy.context.scene.tool_settings 163 | layout = self.layout 164 | pie = layout.menu_pie() 165 | pie.prop(settings, "snap_element", expand=True) 166 | 167 | 168 | #Menu Snap Element 169 | class VIEW3D_OT_SetPivotIndividual(bpy.types.Operator): 170 | bl_label = "Individual Origins" 171 | bl_idname = "object.setpivotindidual" 172 | 173 | @classmethod 174 | def poll(cls, context): 175 | sc = context.space_data 176 | return (sc.type == 'VIEW_3D') 177 | 178 | def execute(self, context): 179 | bpy.context.space_data.pivot_point = "INDIVIDUAL_ORIGINS" 180 | return {'FINISHED'} 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | ################### PIES ##################### 189 | 190 | class VIEW3D_PIE_origin(Menu): 191 | # label is displayed at the center of the pie menu. 192 | bl_label = "Snapping and Origin" 193 | bl_idname = "object.snapping_pie" 194 | 195 | def draw(self, context): 196 | context = bpy.context 197 | layout = self.layout 198 | tool_settings = context.scene.tool_settings 199 | 200 | pie = layout.menu_pie() 201 | 202 | pie.operator("view3d.snap_selected_to_cursor", icon="CLIPUV_HLT") 203 | pie.operator("view3d.snap_cursor_to_selected", icon="CURSOR") 204 | 205 | # set origin to selection in Edit Mode, set origin to cursor in Object mode 206 | if context.active_object: 207 | if context.object.mode == "EDIT": 208 | pie.operator("object.origin_to_selected", icon="OUTLINER_OB_EMPTY") 209 | else: 210 | pie.operator("object.origin_set",icon="EMPTY_DATA", text="Origin to Cursor").type="ORIGIN_CURSOR" 211 | 212 | if context.object.mode == "OBJECT": 213 | pie.operator("object.origin_set",icon="MESH_CUBE", text="Origin to Geometry").type="ORIGIN_GEOMETRY" 214 | else: 215 | pie.operator("object.origin_to_geometry", icon="MESH_CUBE") 216 | else: 217 | pass 218 | 219 | pie.operator("view3d.snap_cursor_to_center", icon="CURSOR") 220 | pie.operator("wm.call_menu_pie", text="Element / Target", icon='PLUS').name = "VIEW3D_PIE_SnapTarget" 221 | 222 | if context.active_object: 223 | if context.object.mode == "EDIT": 224 | if tool_settings.use_mesh_automerge: 225 | pie.prop(tool_settings, "use_mesh_automerge", text="Automerge (ON)", icon='AUTOMERGE_ON') 226 | else: 227 | pie.prop(tool_settings, "use_mesh_automerge", text="Automerge (OFF)", icon='AUTOMERGE_OFF') 228 | else: 229 | pie.operator("object.setpivotindidual", icon="ROTATECOLLECTION") 230 | else: 231 | passn 232 | 233 | pie.operator("scene.toggle_pivot", icon="ROTATECENTER") 234 | 235 | 236 | 237 | ########## REGISTER ############ 238 | 239 | def register(): 240 | bpy.utils.register_class(VIEW3D_OT_toggle_pivot) 241 | bpy.utils.register_class(VIEW3D_OT_origin_to_selected) 242 | bpy.utils.register_class(VIEW3D_OT_origin_to_geometry) 243 | bpy.utils.register_class(VIEW3D_OT_SnapTargetVariable) 244 | bpy.utils.register_class(VIEW3D_OT_SnapElementVariable) 245 | bpy.utils.register_class(VIEW3D_OT_SetPivotIndividual) 246 | bpy.utils.register_class(VIEW3D_PIE_origin) 247 | bpy.utils.register_class(VIEW3D_PIE_SnapElementMenu) 248 | bpy.utils.register_class(VIEW3D_PIE_SnapTarget) 249 | bpy.utils.register_class(VIEW3D_PIE_Snapping_Extras) 250 | 251 | 252 | wm = bpy.context.window_manager 253 | 254 | km = wm.keyconfigs.addon.keymaps.new(name = 'Object Mode') 255 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS', shift=True).properties.name = "object.snapping_pie" 256 | 257 | km = wm.keyconfigs.addon.keymaps.new(name = 'Mesh') 258 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS',shift=True).properties.name = "object.snapping_pie" 259 | 260 | km = wm.keyconfigs.addon.keymaps.new(name = 'Curve') 261 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS',shift=True).properties.name = "object.snapping_pie" 262 | 263 | km = wm.keyconfigs.addon.keymaps.new(name = 'Armature') 264 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS',shift=True).properties.name = "object.snapping_pie" 265 | 266 | km = wm.keyconfigs.addon.keymaps.new(name = 'Pose') 267 | kmi = km.keymap_items.new('wm.call_menu_pie', 'S', 'PRESS',shift=True).properties.name = "object.snapping_pie" 268 | 269 | 270 | 271 | def unregister(): 272 | 273 | bpy.utils.unregister_class(VIEW3D_OT_toggle_pivot) 274 | bpy.utils.unregister_class(VIEW3D_OT_origin_to_selected) 275 | bpy.utils.unregister_class(VIEW3D_OT_origin_to_geometry) 276 | bpy.utils.unregister_class(VIEW3D_OT_SnapTargetVariable) 277 | bpy.utils.unregister_class(VIEW3D_OT_SnapElementVariable) 278 | bpy.utils.unregister_class(VIEW3D_OT_SetPivotIndividual) 279 | bpy.utils.unregister_class(VIEW3D_PIE_origin) 280 | bpy.utils.unregister_class(VIEW3D_PIE_SnapElementMenu) 281 | bpy.utils.unregister_class(VIEW3D_PIE_SnapTarget) 282 | bpy.utils.unregister_class(VIEW3D_PIE_Snapping_Extras) 283 | 284 | 285 | if __name__ == "__main__": 286 | register() 287 | #bpy.ops.wm.call_menu_pie(name="mesh.mesh_operators") 288 | -------------------------------------------------------------------------------- /tracking_pies.py: -------------------------------------------------------------------------------- 1 | # 2 | # This program is free software; you can redistribute it and/or 3 | # modify it under the terms of the GNU General Public License 4 | # as published by the Free Software Foundation; either version 2 5 | # of the License, or (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program; if not, write to the Free Software Foundation, 14 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 15 | # 16 | # ##### END GPL LICENSE BLOCK ##### 17 | 18 | # 19 | 20 | bl_info = { 21 | "name": "Clip Editor Pies: Key: 'hotkey list Below'", 22 | "description": "Clip Editor Pies", 23 | "author": "Antony Riakiotakis, Sebastian Koenig", 24 | "version": (0, 1, 1), 25 | "blender": (2, 77, 0), 26 | "location": "Q, W, Shift W, E, Shift S, Shift A, Shift E", 27 | "warning": "", 28 | "wiki_url": "", 29 | "category": "Pie Menu" 30 | } 31 | 32 | import bpy 33 | from bpy.types import Menu, Operator 34 | 35 | 36 | 37 | ############################## 38 | # CLASSES 39 | ############################## 40 | 41 | 42 | class CLIP_PIE_refine_pie(Menu): 43 | # Refinement Options 44 | bl_label = "Refine Intrinsics" 45 | 46 | @classmethod 47 | def poll(cls, context): 48 | space = context.space_data 49 | return (space.type == 'CLIP_EDITOR') and space.clip 50 | 51 | def draw(self, context): 52 | clip = context.space_data.clip 53 | settings = clip.tracking.settings 54 | 55 | layout = self.layout 56 | pie = layout.menu_pie() 57 | pie.prop(settings, "refine_intrinsics", expand=True) 58 | 59 | 60 | class CLIP_PIE_geometry_reconstruction(Menu): 61 | # Geometry Reconstruction 62 | bl_label = "Reconstruction" 63 | 64 | def draw(self, context): 65 | layout = self.layout 66 | pie = layout.menu_pie() 67 | pie.operator("clip.bundles_to_mesh", icon='MESH_DATA') 68 | pie.operator("clip.track_to_empty", icon='EMPTY_DATA') 69 | 70 | 71 | class CLIP_PIE_display_pie(Menu): 72 | # Display Options 73 | bl_label = "Marker Display" 74 | 75 | def draw(self, context): 76 | space = context.space_data 77 | 78 | layout = self.layout 79 | pie = layout.menu_pie() 80 | pie.prop(space, "show_names", text="Show Track Info", icon='WORDWRAP_ON') 81 | pie.prop(space, "show_disabled", text="Show Disabled Tracks", icon='VISIBLE_IPO_ON') 82 | pie.prop(space, "show_marker_search", text="Display Search Area", icon='VIEWZOOM') 83 | pie.prop(space, "show_marker_pattern", text="Display Pattern Area", icon='BORDERMOVE') 84 | 85 | 86 | class CLIP_PIE_marker_pie(Menu): 87 | # Settings for the individual markers 88 | bl_label = "Marker Settings" 89 | 90 | def draw(self, context): 91 | clip = context.space_data.clip 92 | tracks = getattr(getattr(clip, "tracking", None), "tracks", None) 93 | track_active = tracks.active if tracks else None 94 | 95 | layout = self.layout 96 | pie = layout.menu_pie() 97 | # Use Location Tracking 98 | prop = pie.operator("wm.context_set_enum", text="Loc", icon='OUTLINER_DATA_EMPTY') 99 | prop.data_path = "space_data.clip.tracking.tracks.active.motion_model" 100 | prop.value = "Loc" 101 | # Use Affine Tracking 102 | prop = pie.operator("wm.context_set_enum", text="Affine", icon='OUTLINER_DATA_LATTICE') 103 | prop.data_path = "space_data.clip.tracking.tracks.active.motion_model" 104 | prop.value = "Affine" 105 | # Copy Settings From Active To Selected 106 | pie.operator("clip.track_settings_to_track", icon='COPYDOWN') 107 | # Make Settings Default 108 | pie.operator("clip.track_settings_as_default", icon='SETTINGS') 109 | if track_active: 110 | # Use Normalization 111 | pie.prop(track_active, "use_normalization", text="Normalization") 112 | # Use Brute Force 113 | pie.prop(track_active, "use_brute", text="Use Brute Force") 114 | # Use The blue Channel 115 | pie.prop(track_active, "use_blue_channel", text="Blue Channel") 116 | # Match Either Previous Frame Or Keyframe 117 | if track_active.pattern_match == "PREV_FRAME": 118 | prop = pie.operator("wm.context_set_enum", text="Match Previous", icon='KEYINGSET') 119 | prop.data_path = "space_data.clip.tracking.tracks.active.pattern_match" 120 | prop.value = 'KEYFRAME' 121 | else: 122 | prop = pie.operator("wm.context_set_enum", text="Match Keyframe", icon='KEY_HLT') 123 | prop.data_path = "space_data.clip.tracking.tracks.active.pattern_match" 124 | prop.value = 'PREV_FRAME' 125 | 126 | 127 | class CLIP_PIE_tracking_pie(Menu): 128 | # Tracking Operators 129 | bl_label = "Tracking" 130 | 131 | def draw(self, context): 132 | layout = self.layout 133 | pie = layout.menu_pie() 134 | # Track Backwards 135 | prop = pie.operator("clip.track_markers", icon='PLAY_REVERSE') 136 | prop.backwards = True 137 | prop.sequence = True 138 | # Track Forwards 139 | prop = pie.operator("clip.track_markers", icon='PLAY') 140 | prop.backwards = False 141 | prop.sequence = True 142 | # Disable Marker 143 | pie.operator("clip.disable_markers", icon='RESTRICT_VIEW_ON').action = 'TOGGLE' 144 | # Detect Features 145 | pie.operator("clip.detect_features", icon='ZOOM_SELECTED') 146 | # Clear Path Backwards 147 | pie.operator("clip.clear_track_path", icon='BACK').action = 'UPTO' 148 | # Clear Path Forwards 149 | pie.operator("clip.clear_track_path", icon='FORWARD').action = 'REMAINED' 150 | # Refine Backwards 151 | pie.operator("clip.refine_markers", icon='LOOP_BACK').backwards = True 152 | # Refine Forwards 153 | pie.operator("clip.refine_markers", icon='LOOP_FORWARDS').backwards = False 154 | 155 | 156 | class CLIP_PIE_clipsetup_pie(Menu): 157 | # Setup the clip display options 158 | bl_label = "Clip and Display Setup" 159 | 160 | def draw(self, context): 161 | space = context.space_data 162 | 163 | layout = self.layout 164 | pie = layout.menu_pie() 165 | # Reload Footage 166 | pie.operator("clip.reload", text="Reload Footage", icon='FILE_REFRESH') 167 | # Prefetch Footage 168 | pie.operator("clip.prefetch", text="Prefetch Footage", icon='LOOP_FORWARDS') 169 | # Mute Footage 170 | pie.prop(space, "use_mute_footage", text="Mute Footage", icon='MUTE_IPO_ON') 171 | # Render Undistorted 172 | pie.prop(space.clip_user, "use_render_undistorted", text="Render Undistorted") 173 | # Set Scene Frames 174 | pie.operator("clip.set_scene_frames", text="Set Scene Frames", icon='SCENE_DATA') 175 | # PIE: Marker Display 176 | pie.operator("wm.call_menu_pie", text="Marker Display", icon='PLUS').name = "CLIP_PIE_display_pie" 177 | # Set Active Clip 178 | pie.operator("clip.set_active_clip", icon='CLIP') 179 | # Lock Selection 180 | pie.prop(space, "lock_selection", icon='LOCKED') 181 | 182 | 183 | class CLIP_PIE_solver_pie(Menu): 184 | # Operators to solve the scene 185 | bl_label = "Solving" 186 | 187 | def draw(self, context): 188 | clip = context.space_data.clip 189 | settings = getattr(getattr(clip, "tracking", None), "settings", None) 190 | 191 | layout = self.layout 192 | pie = layout.menu_pie() 193 | # create Plane Track 194 | pie.operator("clip.create_plane_track", icon='MESH_PLANE') 195 | # Solve Camera 196 | pie.operator("clip.solve_camera", text="Solve Camera", icon='OUTLINER_OB_CAMERA') 197 | # PIE: Refinement 198 | if settings: 199 | pie.operator("wm.call_menu_pie", text="Refinement", 200 | icon='CAMERA_DATA').name = "CLIP_PIE_refine_pie" 201 | # Use Tripod Solver 202 | pie.prop(settings, "use_tripod_solver", text="Tripod Solver") 203 | # Set Keyframe A 204 | pie.operator("clip.set_solver_keyframe", text="Set Keyframe A", 205 | icon='KEY_HLT').keyframe = 'KEYFRAME_A' 206 | # Set Keyframe B 207 | pie.operator("clip.set_solver_keyframe", text="Set Keyframe B", 208 | icon='KEY_HLT').keyframe = 'KEYFRAME_B' 209 | # Clean Tracks 210 | prop = pie.operator("clip.clean_tracks", icon='STICKY_UVS_DISABLE') 211 | # Filter Tracks 212 | pie.operator("clip.filter_tracks", icon='FILTER') 213 | prop.frames = 15 214 | prop.error = 2 215 | 216 | 217 | class CLIP_PIE_reconstruction_pie(Menu): 218 | # Scene Reconstruction 219 | bl_label = "Reconstruction" 220 | 221 | def draw(self, context): 222 | layout = self.layout 223 | pie = layout.menu_pie() 224 | # Set Active Clip As Viewport Background 225 | pie.operator("clip.set_viewport_background", text="Set Viewport Background", icon='SCENE_DATA') 226 | # Setup Tracking Scene 227 | pie.operator("clip.setup_tracking_scene", text="Setup Tracking Scene", icon='SCENE_DATA') 228 | # Setup Floor 229 | pie.operator("clip.set_plane", text="Setup Floor", icon='MESH_PLANE') 230 | # Set Origin 231 | pie.operator("clip.set_origin", text="Set Origin", icon='MANIPUL') 232 | # Set X Axis 233 | pie.operator("clip.set_axis", text="Set X Axis", icon='AXIS_FRONT').axis = 'X' 234 | # Set Y Axis 235 | pie.operator("clip.set_axis", text="Set Y Axis", icon='AXIS_SIDE').axis = 'Y' 236 | # Set Scale 237 | pie.operator("clip.set_scale", text="Set Scale", icon='ARROW_LEFTRIGHT') 238 | # PIE: Reconstruction 239 | pie.operator("wm.call_menu_pie", text="Reconstruction", 240 | icon='MESH_DATA').name = "CLIP_PIE_geometry_reconstruction" 241 | 242 | 243 | class CLIP_PIE_timecontrol_pie(Menu): 244 | # Time Controls 245 | bl_label = "Time Control" 246 | 247 | def draw(self, context): 248 | layout = self.layout 249 | pie = layout.menu_pie() 250 | # Jump To Startframe 251 | pie.operator("screen.frame_jump", text="Jump to Startframe", icon='TRIA_LEFT').end = False 252 | # Jump To Endframe 253 | pie.operator("screen.frame_jump", text="Jump to Endframe", icon='TRIA_RIGHT').end = True 254 | # Jump To Start Of The Track 255 | pie.operator("clip.frame_jump", text="Start of Track", icon='REW').position = 'PATHSTART' 256 | # Jump To End of The Track 257 | pie.operator("clip.frame_jump", text="End of Track", icon='FF').position = 'PATHEND' 258 | # Play Backwards 259 | pie.operator("screen.animation_play", text="Playback Backwards", icon='PLAY_REVERSE').reverse = True 260 | # Play Forwards 261 | pie.operator("screen.animation_play", text="Playback Forwards", icon='PLAY').reverse = False 262 | # Go One Frame Back 263 | pie.operator("screen.frame_offset", text="Previous Frame", icon='TRIA_LEFT').delta = -1 264 | # Go One Frame Forwards 265 | pie.operator("screen.frame_offset", text="Next Frame", icon='TRIA_RIGHT').delta = 1 266 | 267 | 268 | 269 | addon_keymaps = [] 270 | 271 | classes = ( 272 | CLIP_OT_select_zero_weighted, 273 | CLIP_OT_weight_fade, 274 | CLIP_PIE_geometry_reconstruction, 275 | CLIP_PIE_tracking_pie, 276 | CLIP_PIE_display_pie, 277 | CLIP_PIE_marker_pie, 278 | CLIP_PIE_solver_pie, 279 | CLIP_PIE_refine_pie, 280 | CLIP_PIE_reconstruction_pie, 281 | CLIP_PIE_clipsetup_pie, 282 | CLIP_PIE_timecontrol_pie 283 | ) 284 | 285 | 286 | def register(): 287 | addon_keymaps.clear() 288 | for cls in classes: 289 | bpy.utils.register_class(cls) 290 | 291 | wm = bpy.context.window_manager 292 | 293 | if wm.keyconfigs.addon: 294 | 295 | km = wm.keyconfigs.addon.keymaps.new(name="Clip", space_type='CLIP_EDITOR') 296 | 297 | kmi = km.keymap_items.new("wm.call_menu_pie", 'Q', 'PRESS') 298 | kmi.properties.name = "CLIP_PIE_marker_pie" 299 | addon_keymaps.append((km, kmi)) 300 | 301 | kmi = km.keymap_items.new("wm.call_menu_pie", 'W', 'PRESS') 302 | kmi.properties.name = "CLIP_PIE_clipsetup_pie" 303 | addon_keymaps.append((km, kmi)) 304 | 305 | kmi = km.keymap_items.new("wm.call_menu_pie", 'E', 'PRESS') 306 | kmi.properties.name = "CLIP_PIE_tracking_pie" 307 | addon_keymaps.append((km, kmi)) 308 | 309 | kmi = km.keymap_items.new("wm.call_menu_pie", 'S', 'PRESS', shift=True) 310 | kmi.properties.name = "CLIP_PIE_solver_pie" 311 | addon_keymaps.append((km, kmi)) 312 | 313 | kmi = km.keymap_items.new("wm.call_menu_pie", 'W', 'PRESS', shift=True) 314 | kmi.properties.name = "CLIP_PIE_reconstruction_pie" 315 | addon_keymaps.append((km, kmi)) 316 | 317 | kmi = km.keymap_items.new("wm.call_menu_pie", 'A', 'PRESS', shift=True) 318 | kmi.properties.name = "CLIP_PIE_timecontrol_pie" 319 | addon_keymaps.append((km, kmi)) 320 | 321 | kmi = km.keymap_items.new("wm.call_menu_pie", 'E', 'PRESS', shift=True) 322 | kmi.properties.name = "CLIP_PIE_tracking_tools" 323 | addon_keymaps.append((km, kmi)) 324 | 325 | 326 | def unregister(): 327 | for cls in classes: 328 | bpy.utils.unregister_class(cls) 329 | 330 | wm = bpy.context.window_manager 331 | kc = wm.keyconfigs.addon 332 | if kc: 333 | for km, kmi in addon_keymaps: 334 | km.keymap_items.remove(kmi) 335 | addon_keymaps.clear() 336 | 337 | 338 | if __name__ == "__main__": 339 | register() 340 | -------------------------------------------------------------------------------- /tracking_pies_280.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | # 15 | # ##### END GPL LICENSE BLOCK ##### 16 | 17 | # 18 | 19 | bl_info = { 20 | "name": "Clip Editor Pies: Key: 'hotkey list Below'", 21 | "description": "Clip Editor Pies", 22 | "author": "Antony Riakiotakis, Sebastian Koenig", 23 | "version": (0, 1, 3), 24 | "blender": (2, 80, 0), 25 | "location": "E, Shift E, Shift D, Shift W, Shift S", 26 | "warning": "", 27 | "wiki_url": "", 28 | "category": "Pie Menu" 29 | } 30 | 31 | import bpy 32 | from bpy.types import Menu, Operator 33 | 34 | ############################## 35 | # CLASSES 36 | # 37 | ############################## 38 | 39 | 40 | class CLIP_PIE_marker_pie(Menu): 41 | # Settings for the individual markers 42 | bl_label = "Marker Settings" 43 | 44 | @classmethod 45 | def poll(cls, context): 46 | space = context.space_data 47 | return space.mode == 'TRACKING' 48 | 49 | def draw(self, context): 50 | clip = context.space_data.clip 51 | tracks = getattr(getattr(clip, "tracking", None), "tracks", None) 52 | track_active = tracks.active if tracks else None 53 | 54 | layout = self.layout 55 | pie = layout.menu_pie() 56 | # Use Location Tracking 57 | prop = pie.operator("wm.context_set_enum", text="Loc", icon='OUTLINER_DATA_EMPTY') 58 | prop.data_path = "space_data.clip.tracking.tracks.active.motion_model" 59 | prop.value = "Loc" 60 | # Use Affine Tracking 61 | prop = pie.operator("wm.context_set_enum", text="Affine", icon='OUTLINER_DATA_LATTICE') 62 | prop.data_path = "space_data.clip.tracking.tracks.active.motion_model" 63 | prop.value = "Affine" 64 | # Copy Settings From Active To Selected 65 | pie.operator("clip.track_settings_to_track", icon='COPYDOWN') 66 | # Make Settings Default 67 | pie.operator("clip.track_settings_as_default", icon='SETTINGS') 68 | if track_active: 69 | # Use Normalization 70 | pie.prop(track_active, "use_normalization", text="Normalization") 71 | # Use Brute Force 72 | pie.prop(track_active, "use_brute", text="Use Brute Force") 73 | # Match Keyframe 74 | prop = pie.operator("wm.context_set_enum", text="Match Previous", icon='KEYFRAME_HLT') 75 | prop.data_path = "space_data.clip.tracking.tracks.active.pattern_match" 76 | prop.value = 'KEYFRAME' 77 | # Match Previous Frame 78 | prop = pie.operator("wm.context_set_enum", text="Match Keyframe", icon='KEYFRAME') 79 | prop.data_path = "space_data.clip.tracking.tracks.active.pattern_match" 80 | prop.value = 'PREV_FRAME' 81 | 82 | 83 | class CLIP_PIE_tracking_pie(Menu): 84 | # Tracking Operators 85 | bl_label = "Tracking" 86 | 87 | @classmethod 88 | def poll(cls, context): 89 | space = context.space_data 90 | return space.mode == 'TRACKING' 91 | 92 | def draw(self, context): 93 | space = context.space_data 94 | clip = space.clip 95 | act_track = clip.tracking.tracks.active 96 | 97 | layout = self.layout 98 | pie = layout.menu_pie() 99 | # Track Backwards 100 | prop = pie.operator("clip.track_markers", icon='TRACKING_BACKWARDS') 101 | prop.backwards = True 102 | prop.sequence = True 103 | # Track Forwards 104 | prop = pie.operator("clip.track_markers", icon='TRACKING_FORWARDS') 105 | prop.backwards = False 106 | prop.sequence = True 107 | # Disable Marker 108 | pie.operator("clip.disable_markers", icon="VISIBLE_IPO_ON").action = 'TOGGLE' 109 | # Detect Features 110 | pie.operator("clip.detect_features", icon='ZOOM_SELECTED') 111 | # Clear Path Backwards 112 | pie.operator("clip.clear_track_path", icon='TRACKING_CLEAR_BACKWARDS').action = 'UPTO' 113 | # Clear Path Forwards 114 | pie.operator("clip.clear_track_path", icon='TRACKING_CLEAR_FORWARDS').action = 'REMAINED' 115 | # Refine Backwards 116 | pie.operator("clip.refine_markers", icon='TRACKING_REFINE_BACKWARDS').backwards = True 117 | # Refine Forwards 118 | pie.operator("clip.refine_markers", icon='TRACKING_REFINE_FORWARDS').backwards = False 119 | 120 | 121 | class CLIP_PIE_clipsetup_pie(Menu): 122 | # Setup the clip display options 123 | bl_label = "Clip and Display Setup" 124 | 125 | def draw(self, context): 126 | space = context.space_data 127 | 128 | layout = self.layout 129 | pie = layout.menu_pie() 130 | # Reload Footage 131 | pie.operator("clip.reload", text="Reload Footage", icon='FILE_REFRESH') 132 | # Prefetch Footage 133 | pie.operator("clip.prefetch", text="Prefetch Footage", icon='LOOP_FORWARDS') 134 | # Show Disabled Markers 135 | pie.prop(space, "show_disabled", text="Show Disabled Markers") 136 | # Set Scene Frames 137 | pie.operator("clip.set_scene_frames", text="Set Scene Frames", icon='SCENE_DATA') 138 | # Render Undistorted 139 | pie.prop(space.clip_user, "use_render_undistorted", text="Render Undistorted") 140 | # Lock Selection 141 | pie.prop(space, "lock_selection", icon='LOCKED') 142 | # Set Active Clip 143 | pie.operator("clip.set_active_clip", icon='CLIP') 144 | # Mute Footage 145 | pie.prop(space, "use_mute_footage", text="Mute Footage", icon='MUTE_IPO_ON') 146 | 147 | 148 | class CLIP_PIE_solver_pie(Menu): 149 | # Operators to solve the scene 150 | bl_label = "Solving" 151 | 152 | @classmethod 153 | def poll(cls, context): 154 | space = context.space_data 155 | return space.mode == 'TRACKING' 156 | 157 | def draw(self, context): 158 | clip = context.space_data.clip 159 | settings = getattr(getattr(clip, "tracking", None), "settings", None) 160 | 161 | layout = self.layout 162 | pie = layout.menu_pie() 163 | # Clear Solution 164 | pie.operator("clip.clear_solution", icon='FILE_REFRESH') 165 | # Solve Camera 166 | pie.operator("clip.solve_camera", text="Solve Camera", icon='OUTLINER_OB_CAMERA') 167 | # Use Tripod Solver 168 | if settings: 169 | pie.prop(settings, "use_tripod_solver", text="Tripod Solver") 170 | # create Plane Track 171 | pie.operator("clip.create_plane_track", icon='MATPLANE') 172 | # Set Keyframe A 173 | pie.operator("clip.set_solver_keyframe", text="Set Keyframe A", 174 | icon='KEYFRAME').keyframe = 'KEYFRAME_A' 175 | # Set Keyframe B 176 | pie.operator("clip.set_solver_keyframe", text="Set Keyframe B", 177 | icon='KEYFRAME').keyframe = 'KEYFRAME_B' 178 | # Clean Tracks 179 | prop = pie.operator("clip.clean_tracks", icon='X') 180 | # Filter Tracks 181 | pie.operator("clip.filter_tracks", icon='FILTER') 182 | prop.frames = 15 183 | prop.error = 2 184 | 185 | 186 | class CLIP_PIE_reconstruction_pie(Menu): 187 | # Scene Reconstruction 188 | bl_label = "Reconstruction" 189 | 190 | @classmethod 191 | def poll(cls, context): 192 | space = context.space_data 193 | return space.mode == 'TRACKING' 194 | 195 | def draw(self, context): 196 | layout = self.layout 197 | pie = layout.menu_pie() 198 | # Set Active Clip As Viewport Background 199 | pie.operator("clip.set_viewport_background", text="Set Viewport Background", icon='FILE_IMAGE') 200 | # Setup Tracking Scene 201 | pie.operator("clip.setup_tracking_scene", text="Setup Tracking Scene", icon='SCENE_DATA') 202 | # Setup Floor 203 | pie.operator("clip.set_plane", text="Setup Floor", icon='MESH_PLANE') 204 | # Set Origin 205 | pie.operator("clip.set_origin", text="Set Origin", icon='OBJECT_ORIGIN') 206 | # Set X Axis 207 | pie.operator("clip.set_axis", text="Set X Axis", icon='AXIS_FRONT').axis = 'X' 208 | # Set Y Axis 209 | pie.operator("clip.set_axis", text="Set Y Axis", icon='AXIS_SIDE').axis = 'Y' 210 | # Set Scale 211 | pie.operator("clip.set_scale", text="Set Scale", icon='ARROW_LEFTRIGHT') 212 | # Apply Solution Scale 213 | pie.operator("clip.apply_solution_scale", icon='ARROW_LEFTRIGHT') 214 | 215 | 216 | 217 | addon_keymaps = [] 218 | 219 | classes = ( 220 | CLIP_PIE_tracking_pie, 221 | CLIP_PIE_marker_pie, 222 | CLIP_PIE_solver_pie, 223 | CLIP_PIE_reconstruction_pie, 224 | CLIP_PIE_clipsetup_pie, 225 | ) 226 | 227 | 228 | def register(): 229 | addon_keymaps.clear() 230 | for cls in classes: 231 | bpy.utils.register_class(cls) 232 | 233 | wm = bpy.context.window_manager 234 | 235 | if wm.keyconfigs.addon: 236 | 237 | km = wm.keyconfigs.addon.keymaps.new(name="Clip", space_type='CLIP_EDITOR') 238 | 239 | kmi = km.keymap_items.new("wm.call_menu_pie", 'E', 'PRESS', shift=True) 240 | kmi.properties.name = "CLIP_PIE_marker_pie" 241 | addon_keymaps.append((km, kmi)) 242 | 243 | kmi = km.keymap_items.new("wm.call_menu_pie", 'D', 'PRESS', shift=True) 244 | kmi.properties.name = "CLIP_PIE_clipsetup_pie" 245 | addon_keymaps.append((km, kmi)) 246 | 247 | kmi = km.keymap_items.new("wm.call_menu_pie", 'E', 'PRESS') 248 | kmi.properties.name = "CLIP_PIE_tracking_pie" 249 | addon_keymaps.append((km, kmi)) 250 | 251 | kmi = km.keymap_items.new("wm.call_menu_pie", 'S', 'PRESS', shift=True) 252 | kmi.properties.name = "CLIP_PIE_solver_pie" 253 | addon_keymaps.append((km, kmi)) 254 | 255 | kmi = km.keymap_items.new("wm.call_menu_pie", 'W', 'PRESS', shift=True) 256 | kmi.properties.name = "CLIP_PIE_reconstruction_pie" 257 | addon_keymaps.append((km, kmi)) 258 | 259 | kmi = km.keymap_items.new("wm.call_menu_pie", 'A', 'PRESS', shift=True) 260 | kmi.properties.name = "CLIP_PIE_timecontrol_pie" 261 | addon_keymaps.append((km, kmi)) 262 | 263 | 264 | def unregister(): 265 | for cls in classes: 266 | bpy.utils.unregister_class(cls) 267 | 268 | wm = bpy.context.window_manager 269 | kc = wm.keyconfigs.addon 270 | if kc: 271 | for km, kmi in addon_keymaps: 272 | km.keymap_items.remove(kmi) 273 | addon_keymaps.clear() 274 | 275 | 276 | if __name__ == "__main__": 277 | register() 278 | -------------------------------------------------------------------------------- /tracking_tools.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 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. 7 | # 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. 12 | # 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. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Tracking Tools", 21 | "author": "Sebastian Koenig", 22 | "version": (0,1), 23 | "blender": (2, 79, 0), 24 | "location": "Clip Editor", 25 | "description": "A couple of tools to make tracking a bit easier", 26 | "warning": "", 27 | "wiki_url": "", 28 | "category": "Tracking" 29 | } 30 | 31 | import bpy 32 | from bpy.types import Operator, Panel, Menu 33 | from mathutils import Vector 34 | 35 | def visible_selected(context): 36 | # return all selected tracks that are not hidden 37 | all_tracks = context.space_data.clip.tracking.tracks 38 | tracks = [t for t in all_tracks if t.select and not t.hide] 39 | return tracks 40 | 41 | def invisible_selected(context): 42 | # if show_disabled is on, we don't see all markers, even though they might be selected 43 | tracks = context.space_data.clip.tracking.tracks 44 | f = context.scene.frame_current 45 | invisible_tracks = [] 46 | # if show_disabled is on, we don't need to check 47 | if not context.space_data.show_disabled: 48 | for t in tracks: 49 | # for the current frame check which tracks are selected but not visible 50 | if not t.select: 51 | continue 52 | # only if the marker has data and is not muted it will be displayed, so skip those 53 | if t.markers.find_frame(f) and not t.markers.find_frame(f).mute: 54 | continue 55 | invisible_tracks.append(t) 56 | return invisible_tracks 57 | 58 | 59 | def get_marker_coordinates_in_pixels(context, track, frame_number): 60 | width, height = context.space_data.clip.size 61 | # return the marker coordinates in relation to the clip 62 | marker = track.markers.find_frame(frame_number) 63 | vector = Vector((marker.co[0] * width, marker.co[1] * height)) 64 | return vector 65 | 66 | 67 | def marker_velocity(context, track, frame): 68 | marker_a = get_marker_coordinates_in_pixels(context, track, frame) 69 | marker_b = get_marker_coordinates_in_pixels(context, track, frame-1) 70 | marker_velocity = marker_a - marker_b 71 | return marker_velocity 72 | 73 | 74 | def get_difference(track_slope, average_slope, axis): 75 | # return the difference between slope of last frame and the average slope before it 76 | difference = track_slope[axis] - average_slope[axis] 77 | # rather use abs difference, to be able to better compare the actual value 78 | difference = abs(difference) 79 | return difference 80 | 81 | 82 | def check_eval_time(track, frame, eval_time): 83 | # check each frame for the evaluation time 84 | list = [] 85 | for f in range(frame-eval_time, frame): 86 | # if there are no markers for that frame, skip 87 | if not track.markers.find_frame(f): 88 | continue 89 | # it also doesnt make sense to use a track that is has no previous marker 90 | if not track.markers.find_frame(f-1): 91 | continue 92 | # the frame after the last track is muted, but still valid, so skip that 93 | if track.markers.find_frame(f).mute: 94 | continue 95 | if track.markers.find_frame(f-1).mute: 96 | continue 97 | # append frames to the list 98 | list.append(f) 99 | # make sure there are no gaps in the list 100 | if len(list) == eval_time: 101 | return True 102 | 103 | 104 | def get_valid_tracks(scene, tracks): 105 | valid_tracks = {} 106 | for t in tracks: 107 | list = [] 108 | for f in range(scene.frame_start, scene.frame_end): 109 | if not t.markers.find_frame(f): 110 | continue 111 | if t.markers.find_frame(f).mute: 112 | continue 113 | if not t.markers.find_frame(f-1): 114 | continue 115 | list.append(f) 116 | valid_tracks[t] = list 117 | return valid_tracks 118 | 119 | 120 | def get_slope(context, track, frame): 121 | print(track.name, frame) 122 | v1 = marker_velocity(context, track, frame) 123 | v2 = marker_velocity(context, track, frame-1) 124 | slope = v1-v2 125 | return slope 126 | 127 | 128 | def get_average_slope(context, track, frame, eval_time): 129 | average = Vector().to_2d() 130 | for f in range(frame-eval_time, frame): 131 | average = get_slope(context, track, f) 132 | average += average 133 | average = average / eval_time 134 | return average 135 | 136 | 137 | def get_marker_list(scene, tracks, fade_time): 138 | # generate a dictionary of tracks that meet the needed conditions 139 | marker_dict = {} 140 | # minimum length should be twice the time we use to fade in/out 141 | threshold = fade_time * 2 142 | for t in tracks: 143 | # only operate on selected tracks that are not hidden 144 | if t.select and not t.hide: 145 | # generate a list of all tracked frames 146 | list = [] 147 | for i in range(scene.frame_start, scene.frame_end): 148 | if t.markers.find_frame(i): 149 | list.append(i) 150 | # if the list is longer than the threshold, add the list and the track to a dict 151 | # (a shorter list wouldn't make much sense) 152 | if len(list) > threshold: 153 | marker_dict[t] = list 154 | return marker_dict 155 | 156 | 157 | def clear_weight_animation(scene, tracks, weight): 158 | zero_weighted = find_zero_weighted_tracks(scene, tracks) 159 | for t in tracks: 160 | for i in range(scene.frame_start, scene.frame_end+1): 161 | try: 162 | t.keyframe_delete(data_path="weight", frame=i) 163 | except: 164 | pass 165 | # set the weight back to 1 unless it's a zero weighted track 166 | if not t in zero_weighted: 167 | t.weight = weight 168 | 169 | 170 | def find_zero_weighted_tracks(scene, tracks): 171 | current_frame = scene.frame_current 172 | list = [] 173 | for t in tracks: 174 | list.append(t) 175 | for f in range(scene.frame_start, scene.frame_end): 176 | scene.frame_set(f) 177 | for t in list: 178 | if t.weight>0: 179 | list.remove(t) 180 | scene.frame_set(current_frame) 181 | return list 182 | 183 | 184 | def insert_keyframe(scene, fade_time, marker_dict): 185 | for track, list in marker_dict.items(): 186 | # define keyframe_values 187 | f1 = list[0] 188 | f2 = list[0] + fade_time 189 | f3 = list[-2] - fade_time 190 | f4 = list[-2] 191 | # only key track start if it is not the start of the clip 192 | if f1 - scene.frame_start > fade_time: 193 | track.weight = 0 194 | track.keyframe_insert(data_path="weight", frame=f1) 195 | track.weight = 1 196 | track.keyframe_insert(data_path="weight", frame=f2) 197 | # now set keyframe for weight 0 at the end of the track 198 | # but only if it doesnt go until the end of the shot 199 | if scene.frame_end - f4+1 > fade_time: 200 | track.keyframe_insert(data_path="weight", frame=f3) 201 | track.weight = 0 202 | track.keyframe_insert(data_path="weight", frame=f4) 203 | 204 | 205 | 206 | ############################## 207 | # CLASSES 208 | ############################## 209 | 210 | 211 | class CLIP_OT_filter_track_ends(Operator): 212 | '''Filter the Track for spikes at the end of a track''' 213 | bl_idname = "clip.filter_track_ends" 214 | bl_label = "Filter Track Ends" 215 | bl_options = {'REGISTER', 'UNDO'} 216 | 217 | eval_time = bpy.props.IntProperty( 218 | name="Evaluation Time", 219 | default=10, 220 | min=0, 221 | max=1000, 222 | description="The length of the last part of the track that should be filtered") 223 | 224 | threshold = bpy.props.IntProperty( 225 | name="Threshold", 226 | default=1, 227 | min=0, 228 | max=100, 229 | description="The threshold over which a marker is considered outlier") 230 | 231 | @staticmethod 232 | def filter_track_ends(context, threshold, eval_time): 233 | # compare the last frame's slope with the ones before, and if needed, mute it. 234 | tracks = context.space_data.clip.tracking.tracks 235 | valid_tracks = get_valid_tracks(context.scene, tracks) 236 | to_clean = {} 237 | for track, list in valid_tracks.items(): 238 | f = list[-1] 239 | # first get the slope of the current track on current frame 240 | track_slope = get_slope(context, track, f) 241 | # if the track is as long as the evaluation time, calculate the average slope 242 | if check_eval_time(track, f, eval_time): 243 | average_slope = Vector().to_2d() 244 | for i in range(f-eval_time, f): 245 | # get the slopes of all frames during the evaluation time 246 | av_slope = get_slope(context, track, i) 247 | average_slope += av_slope 248 | average_slope = average_slope / eval_time 249 | # check abs difference for both values in the vector 250 | for i in [0,1]: 251 | # if the difference between average_slope and track_slope on any axis is above threshold, 252 | # add to the to_clean dictionary 253 | if not track in to_clean and get_difference(track_slope, average_slope, i) > threshold: 254 | to_clean[track] = f 255 | # now we can disable the last frame of the identified tracks 256 | for track, frame in to_clean.items(): 257 | print("cleaned ", track.name, "on frame ", frame) 258 | track.markers.find_frame(frame).mute=True 259 | return len(to_clean) 260 | 261 | @classmethod 262 | def poll(cls, context): 263 | space = context.space_data 264 | return (space.type == 'CLIP_EDITOR') and space.clip 265 | 266 | def execute(self, context): 267 | # first do a minimal cleanup 268 | bpy.ops.clip.clean_tracks(frames=3, error=0, action='DELETE_SEGMENTS') 269 | num_tracks = self.filter_track_ends(context, self.threshold, self.eval_time) 270 | self.report({'INFO'}, "Muted %d track ends" % num_tracks) 271 | return {'FINISHED'} 272 | 273 | 274 | class CLIP_OT_select_foreground(Operator): 275 | '''Select Tracks whose average velocity deviates from the rest. \n Usually the case with tracks near to the camera ''' 276 | bl_idname = "clip.select_foreground" 277 | bl_label = "Select Foreground Tracks" 278 | bl_options = {'REGISTER', 'UNDO'} 279 | 280 | eval_time = bpy.props.IntProperty( 281 | name="Evaluation Time", 282 | default=20, 283 | min=0, 284 | max=1000, 285 | description="The length of the last part of the track that should be filtered") 286 | 287 | threshold = bpy.props.IntProperty( 288 | name="Threshold", 289 | default=2, 290 | min=0, 291 | max=100, 292 | description="The threshold over which a marker is considered outlier") 293 | 294 | @staticmethod 295 | def select_foreground(context, eval_time, threshold): 296 | # filter tracks that move a lot faster than others towards the end of the track 297 | tracks = context.space_data.clip.tracking.tracks 298 | valid_tracks = get_valid_tracks(context.scene, tracks) 299 | foreground = [] 300 | for track, list in valid_tracks.items(): 301 | f = list[-1] 302 | # first get the average of the last frame during evaluation time 303 | if check_eval_time(track, f, eval_time) and not track in foreground: 304 | track_average = get_average_slope(context, track, f, eval_time) 305 | # then get the average of all other tracks 306 | global_average = Vector().to_2d() 307 | currently_valid_tracks = [] 308 | # first check if the other tracks are valid too. 309 | for t in tracks: 310 | if check_eval_time(t, f, eval_time) and not t == track: 311 | currently_valid_tracks.append(t) 312 | for t in currently_valid_tracks: 313 | other_average = get_average_slope(context, t, f, eval_time) 314 | global_average += other_average 315 | global_average = global_average / len(currently_valid_tracks) 316 | for i in [0,1]: 317 | difference = get_difference(track_average, global_average, i) * eval_time 318 | print(track.name, i, difference) 319 | if difference > threshold: 320 | foreground.append(track) 321 | for track in foreground: 322 | track.select = True 323 | 324 | @classmethod 325 | def poll(cls, context): 326 | space = context.space_data 327 | return (space.type == 'CLIP_EDITOR') and space.clip 328 | 329 | def execute(self, context): 330 | scene = context.scene 331 | self.select_foreground(context, self.eval_time, self.threshold) 332 | return {'FINISHED'} 333 | 334 | 335 | class CLIP_OT_select_zero_weighted_tracks(Operator): 336 | '''Select all tracks that have a marker weight of zero throughout the entire shot''' 337 | bl_idname = "clip.select_zero_weighted_tracks" 338 | bl_label = "Select Zero Weighted Tracks" 339 | 340 | @classmethod 341 | def poll(cls, context): 342 | space = context.space_data 343 | return (space.type == 'CLIP_EDITOR') and space.clip 344 | 345 | def execute(self, context): 346 | scene = context.scene 347 | tracks = context.space_data.clip.tracking.tracks 348 | zero_weighted = find_zero_weighted_tracks(scene, tracks) 349 | for t in zero_weighted: 350 | t.select = True 351 | return {'FINISHED'} 352 | 353 | 354 | class CLIP_OT_weight_fade(Operator): 355 | '''Fade in and out the weight of selected markers''' 356 | bl_idname = "clip.weight_fade" 357 | bl_label = "Fade Marker Weight" 358 | bl_options = {'REGISTER', 'UNDO'} 359 | 360 | fade_time = bpy.props.IntProperty(name="Fade Time", 361 | default=10, min=0, max=100) 362 | 363 | @classmethod 364 | def poll(cls, context): 365 | space = context.space_data 366 | return (space.type == 'CLIP_EDITOR') and space.clip 367 | 368 | def execute(self, context): 369 | scene = context.scene 370 | tracks = visible_selected(context) 371 | # first clear any previous weight animation 372 | clear_weight_animation(scene, tracks, 1) 373 | # then find out which tracks to operate on 374 | valid_tracks = get_valid_tracks(scene, tracks) 375 | short = [] 376 | for track, list in valid_tracks.items(): 377 | if len(list) < self.fade_time * 2: 378 | short.append(track) 379 | print(short) 380 | for t in short: 381 | del valid_tracks[t] 382 | # then insert the weight keyframes 383 | insert_keyframe(scene, self.fade_time, valid_tracks) 384 | return {'FINISHED'} 385 | 386 | 387 | class CLIP_OT_clear_weight_animation(Operator): 388 | '''Clear any weight animation of the selected tracks''' 389 | bl_idname = "clip.clear_weight_animation" 390 | bl_label = "Clear Weight Animation" 391 | bl_options = {'REGISTER', 'UNDO'} 392 | 393 | @classmethod 394 | def poll(cls, context): 395 | space = context.space_data 396 | return (space.type == 'CLIP_EDITOR') and space.clip 397 | 398 | def execute(self, context): 399 | scene = context.scene 400 | tracks = visible_selected(context) 401 | clear_weight_animation(scene, tracks, 1) 402 | return {'FINISHED'} 403 | 404 | 405 | class CLIP_OT_mesh_reconstruction(Operator): 406 | ''' Create a face from selected tracks. \n Needs a camera solve! Works best for flat surfaces''' 407 | bl_idname = "clip.mesh_reconstruction" 408 | bl_label = "Mesh Reconstruction" 409 | bl_options = {'UNDO', 'REGISTER'} 410 | 411 | @classmethod 412 | def poll(cls, context): 413 | space = context.space_data 414 | return (space.type == "CLIP_EDITOR") and space.clip 415 | 416 | def execute(self, context): 417 | tracks = visible_selected(context) 418 | invisibles = len(invisible_selected(context)) 419 | invisibles_warning = "Attention, there are %d selected tracks you don't see, due to 'show_disabled'. " % invisibles 420 | number_warning = "The number of selected tracks indicates that you might try to generate a non-flat surface. That might not work as expected." 421 | # if there are enough tracks to form a mesh, abort 422 | if len(tracks) < 3: 423 | self.report({'ERROR'}, "You need at least 3 selected and solved tracks in order to generate a mesh.") 424 | else: 425 | # if there are tracks selected, but not displayed, show a warning 426 | if invisibles > 0: 427 | if len(tracks) > 8: 428 | self.report({'WARNING'}, invisibles_warning + number_warning) 429 | else: 430 | self.report({'WARNING'}, invisibles_warning) 431 | elif len(tracks) > 8: 432 | self.report({'WARNING'}, number_warning) 433 | 434 | # make a mesh from selected markers, called "Tracks" 435 | bpy.ops.clip.bundles_to_mesh() 436 | # create a plane from the single vertices 437 | ob = bpy.data.objects["Tracks"] 438 | bpy.context.scene.objects.active = ob 439 | ob.select = True 440 | bpy.ops.object.mode_set(mode="EDIT") 441 | bpy.ops.mesh.select_all(action="SELECT") 442 | bpy.ops.mesh.edge_face_add() 443 | bpy.ops.object.mode_set(mode="OBJECT") 444 | # rename the object so that you can create new objects (called "Tracks") 445 | ob.name = "TrackMesh" 446 | return {'FINISHED'} 447 | 448 | 449 | class CLIP_OT_goto_next_marker_gap(Operator): 450 | '''Find the next part of the shot with less than 8 markers''' 451 | bl_idname = "clip.goto_next_marker_gap" 452 | bl_label = "Goto Next Marker Gap" 453 | 454 | @classmethod 455 | def poll(cls, context): 456 | space = context.space_data 457 | return (space.type == "CLIP_EDITOR") and space.clip 458 | 459 | def execute(self, context): 460 | scene = context.scene 461 | tracks = context.space_data.clip.tracking.tracks 462 | # for each frame of the shot check the number of tracked markers 463 | for f in range(scene.frame_start, scene.frame_end): 464 | marker_list = [track for track in tracks if track.markers.find_frame(f)] 465 | # as soon as there are less than 8 markers, set the cursor there and stop 466 | if len(marker_list) < 8: 467 | if not f == scene.frame_start: 468 | scene.frame_current = f-1 469 | else: 470 | scene.frame_current = f 471 | break 472 | return ({'FINISHED'}) 473 | 474 | 475 | class CLIP_OT_create_zero_weighted_tracks(Operator): 476 | ''' Turn Selected Tracks into Zero Weighted Tracks.''' 477 | bl_idname = "clip.create_zero_weighted_tracks" 478 | bl_label = "Create Zero Weighted Tracks" 479 | 480 | @classmethod 481 | def poll(cls, context): 482 | space = context.space_data 483 | return (space.type == "CLIP_EDITOR") and space.clip 484 | 485 | def execute(self, context): 486 | scene = context.scene 487 | all_tracks = visible_selected(context) 488 | invisibles = invisible_selected(context) 489 | # make sure we don't operate on markers that are currently not visible 490 | tracks = [t for t in all_tracks if not t in invisibles] 491 | clear_weight_animation(scene, tracks, 0) 492 | return {'FINISHED'} 493 | 494 | 495 | ########################################### 496 | ### UI ################################### 497 | ######################################### 498 | 499 | class CLIP_PT_weight_fade_panel(Panel): 500 | bl_idname = "clip.tracking_tools" 501 | bl_label = "Tracking Tools" 502 | bl_space_type = "CLIP_EDITOR" 503 | bl_region_type = "TOOLS" 504 | bl_category = "Solve" 505 | 506 | def draw(self, context): 507 | layout = self.layout 508 | col = layout.column(align=True) 509 | col.operator("clip.filter_track_ends") 510 | col.operator("clip.select_foreground") 511 | col.operator("clip.weight_fade") 512 | col.operator("clip.select_zero_weighted_tracks") 513 | col.operator("clip.create_zero_weighted_tracks") 514 | col.operator("clip.mesh_reconstruction") 515 | 516 | 517 | class CLIP_PIE_tracking_tools(Menu): 518 | bl_label = "Tracking Tools" 519 | 520 | def draw(self, context): 521 | layout = self.layout 522 | pie = layout.menu_pie() 523 | pie.operator("clip.goto_next_marker_gap", icon="LOOP_BACK") 524 | pie.operator("clip.select_zero_weighted_tracks", icon="RESTRICT_SELECT_ON") 525 | pie.operator("clip.create_zero_weighted_tracks", icon="GHOST_ENABLED") 526 | pie.operator("clip.filter_track_ends", icon="IPO_ELASTIC") 527 | pie.operator("clip.clear_weight_animation", icon="ANIM") 528 | pie.operator("clip.select_foreground", icon="IPO") 529 | pie.operator("clip.weight_fade", icon="PMARKER_SEL") 530 | pie.operator("clip.track_copy_color", icon="COLOR") 531 | # pie.operator("clip.mesh_reconstruction", icon="OUTLINER_OB_MESH") 532 | 533 | 534 | ################### 535 | # REGISTER 536 | ################### 537 | addon_keymaps = [] 538 | 539 | classes = ( 540 | CLIP_OT_weight_fade, 541 | CLIP_OT_select_foreground, 542 | CLIP_OT_filter_track_ends, 543 | CLIP_OT_clear_weight_animation, 544 | CLIP_OT_select_zero_weighted_tracks, 545 | CLIP_OT_create_zero_weighted_tracks, 546 | CLIP_OT_mesh_reconstruction, 547 | CLIP_PT_weight_fade_panel, 548 | CLIP_OT_goto_next_marker_gap, 549 | CLIP_PIE_tracking_tools 550 | ) 551 | 552 | def register(): 553 | addon_keymaps.clear() 554 | for c in classes: 555 | bpy.utils.register_class(c) 556 | 557 | wm = bpy.context.window_manager 558 | km = wm.keyconfigs.addon.keymaps.new(name='Clip', space_type='CLIP_EDITOR') 559 | kmi = km.keymap_items.new('clip.weight_fade', 'W', 'PRESS', alt=True) 560 | 561 | if wm.keyconfigs.addon: 562 | kmi = km.keymap_items.new("wm.call_menu_pie", 'E', 'PRESS', shift=True) 563 | kmi.properties.name = "CLIP_PIE_tracking_tools" 564 | addon_keymaps.append((km, kmi)) 565 | 566 | def unregister(): 567 | for c in classes: 568 | bpy.utils.unregister_class(c) 569 | 570 | if __name__ == "__main__": 571 | register() 572 | -------------------------------------------------------------------------------- /viewport_pies.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Viewport Pies", 3 | "author": "Sebastian Koenig", 4 | "version": (0, 3), 5 | "blender": (2, 7, 2), 6 | "description": "Viewport Pies", 7 | "category": "3D View",} 8 | 9 | 10 | 11 | import bpy 12 | from bpy.types import Menu 13 | 14 | 15 | 16 | ########## CUSTOM OPERATORS ############### 17 | 18 | 19 | class VIEW3D_OT_show_all_wires(bpy.types.Operator): 20 | """Show all wires for selected objects""" 21 | bl_idname = "object.show_all_wires" 22 | bl_label = "Show all wires" 23 | 24 | @classmethod 25 | def poll(cls, context): 26 | space = context.space_data 27 | return space.type == 'VIEW_3D' 28 | 29 | def show_all_wires(context): 30 | wired = [] 31 | clean = [] 32 | obs=[] 33 | for ob in bpy.context.selected_objects: 34 | if ob.type=="MESH" or ob.type=="CURVE": 35 | obs.append(ob) 36 | for ob in obs: 37 | if ob.show_wire: 38 | wired.append(ob) 39 | for ob in obs: 40 | if len(wired)>=1: 41 | ob.show_wire = False 42 | ob.show_all_edges = False 43 | else: 44 | ob.show_wire = True 45 | ob.show_all_edges = True 46 | 47 | 48 | def execute(self, context): 49 | self.show_all_wires() 50 | return {'FINISHED'} 51 | 52 | 53 | 54 | class VIEW3D_OT_draw_wire_only(bpy.types.Operator): 55 | """Show all wires for selected objects""" 56 | bl_idname = "object.draw_wire_only" 57 | bl_label = "Draw Only Wire" 58 | 59 | @classmethod 60 | def poll(cls, context): 61 | space = context.space_data 62 | return space.type == 'VIEW_3D' 63 | 64 | def draw_only_wire(context): 65 | wired = [] 66 | normal = [] 67 | obs=[] 68 | for ob in bpy.context.selected_objects: 69 | if ob.type=="MESH" or ob.type=="CURVE": 70 | obs.append(ob) 71 | for ob in obs: 72 | if ob.draw_type=="WIRE": 73 | wired.append(ob) 74 | for ob in obs: 75 | if len(wired)>=1: 76 | ob.draw_type = "TEXTURED" 77 | else: 78 | ob.draw_type = "WIRE" 79 | 80 | 81 | def execute(self, context): 82 | self.draw_only_wire() 83 | return {'FINISHED'} 84 | 85 | 86 | 87 | 88 | 89 | ################### PIES ##################### 90 | 91 | class VIEW3D_PIE_display(Menu): 92 | # label is displayed at the center of the pie menu. 93 | bl_label = "Viewport Pies" 94 | bl_idname = "object.display_pie" 95 | 96 | def draw(self, context): 97 | context = bpy.context 98 | layout = self.layout 99 | space = context.space_data 100 | 101 | pie = layout.menu_pie() 102 | 103 | pie.prop(space, "show_only_render", icon="SMOOTH") 104 | pie.prop(space, "show_textured_solid", icon="TEXTURE") 105 | pie.operator("object.draw_wire_only", icon="WIRE") 106 | pie.operator("object.show_all_wires", icon="WIRE") 107 | pie.prop(space, "use_matcap") 108 | pie.operator("view3d.view_selected", icon="ZOOM_SELECTED") 109 | pie.operator("view3d.view_all", icon="RESTRICT_VIEW_OFF").center=False 110 | pie.operator("view3d.localview", icon="VIEWZOOM") 111 | 112 | 113 | 114 | 115 | ########## REGISTER ############ 116 | 117 | def register(): 118 | bpy.utils.register_class(VIEW3D_OT_show_all_wires) 119 | bpy.utils.register_class(VIEW3D_OT_draw_wire_only) 120 | bpy.utils.register_class(VIEW3D_PIE_display) 121 | 122 | 123 | wm = bpy.context.window_manager 124 | 125 | km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D') 126 | kmi = km.keymap_items.new('wm.call_menu_pie', 'C', 'PRESS', shift=True).properties.name = "object.display_pie" 127 | 128 | 129 | 130 | 131 | 132 | 133 | def unregister(): 134 | 135 | bpy.utils.unregister_class(VIEW3D_OT_show_all_wires) 136 | bpy.utils.unregister_class(VIEW3D_OT_draw_wire_only) 137 | bpy.utils.unregister_class(VIEW3D_PIE_display) 138 | 139 | 140 | if __name__ == "__main__": 141 | register() 142 | -------------------------------------------------------------------------------- /vr_recorder.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from pathlib import Path 4 | from bpy.props import StringProperty, CollectionProperty, IntProperty, BoolProperty, PointerProperty 5 | from bpy.types import Operator, Panel, UIList, PropertyGroup 6 | 7 | 8 | def stop_recording(scene): 9 | '''automatically stop recording when animation has reached the last frame''' 10 | if scene.frame_current == scene.frame_end: 11 | bpy.ops.scene.vp_stop_recording() 12 | 13 | def create_recorder_empty(context, name): 14 | """ create recorder object and create action if necessary """ 15 | scene = context.scene 16 | # create object if necessary 17 | if not name in bpy.data.objects: 18 | object = bpy.data.objects.new(name, None) 19 | else: 20 | object = bpy.data.objects.get(name) 21 | 22 | # create action if necessary 23 | if not context.scene.vp_action_overwrite: 24 | if not object.animation_data: 25 | object.animation_data_create() 26 | action_name = f'{scene.vp_action_name}_{name[:3].upper()}' 27 | action = bpy.data.actions.new(action_name) 28 | object.animation_data.action = action 29 | object.animation_data.action.use_fake_user = True 30 | 31 | return object 32 | 33 | 34 | def record_handler(scene, cam_ob): 35 | ''' Write keyframes to action of Camera_helper_Empty ''' 36 | frame = scene.frame_current 37 | # get the object controlled by the tracker, to get its motion 38 | vr_cam = bpy.data.objects.get(scene.vp_camera) 39 | cam_ob = bpy.data.objects.get("Camera_helper_Empty") 40 | cam_ob.matrix_world = vr_cam.matrix_world 41 | # write keyframes 42 | # cam_ob.rotation_euler[1] = cam_ob.rotation_euler[1] - vr_cam.rotation_euler[1] 43 | cam_ob.keyframe_insert(data_path="location", frame=frame) 44 | cam_ob.keyframe_insert(data_path="rotation_euler", frame=frame) 45 | 46 | 47 | class ListItem(PropertyGroup): 48 | '''List of shot actions''' 49 | name: StringProperty( 50 | name="ActionName", 51 | description="Name of the action", 52 | default="shot" 53 | ) 54 | 55 | 56 | class VP_UL_shot_list(UIList): 57 | '''List UI of shot actions''' 58 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 59 | custom_icon = 'OBJECT_DATAMODE' 60 | 61 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 62 | layout.prop(item, "name", text="", icon_value=icon, emboss=False) 63 | 64 | elif self.layout_type in {'GRID'}: 65 | layout.alignment='CENTER' 66 | layout.label(text="", icon=custom_icon) 67 | 68 | 69 | class VP_OT_add_shot(Operator): 70 | '''Add a new shot''' 71 | bl_idname = "scene.add_vp_shot" 72 | bl_label = "Add VP Shot" 73 | 74 | def execute(self, context): 75 | context.scene.vp_shot_list.add() 76 | return {'FINISHED'} 77 | 78 | 79 | class VP_OT_play_shot(Operator): 80 | '''Play the selected shot''' 81 | bl_idname = "scene.vp_play_shot" 82 | bl_label = "Play Shot" 83 | 84 | @classmethod 85 | def poll(cls, context): 86 | return bpy.data.objects.get(context.scene.vp_camera) 87 | 88 | def modal(self, context, event): 89 | '''run modal until we cancel''' 90 | scene = context.scene 91 | if event.type in {'RIGHTMOUSE', 'ESC'}: 92 | self.cancel(context) 93 | return {'CANCELLED'} 94 | if scene.frame_current == scene.frame_end: 95 | self.cancel(context) 96 | return {'FINISHED'} 97 | return {'PASS_THROUGH'} 98 | 99 | def execute(self, context): 100 | data = bpy.data 101 | scene = context.scene 102 | # get the action by addressing the index 103 | index = context.scene.vp_shot_list_index 104 | action = data.actions[index] 105 | 106 | # initirate handler 107 | wm = context.window_manager 108 | wm.modal_handler_add(self) 109 | vp_camera = data.objects.get(scene.vp_camera) 110 | vp_camera.hide_viewport = True 111 | 112 | # create player camera if necessary 113 | if not "temp_player_cam" in data.cameras: 114 | player_cam = data.cameras.new("temp_player_cam") 115 | else: 116 | player_cam = data.cameras.get("temp_player_cam") 117 | 118 | if not "temp_player_camera" in data.objects: 119 | player = data.objects.new("temp_player_camera", player_cam) 120 | else: 121 | player = data.objects.get("temp_player_camera") 122 | 123 | player.data = player_cam 124 | # configure camera data 125 | player_cam.lens = vp_camera.data.lens 126 | player_cam.sensor_width = vp_camera.data.sensor_width 127 | # link player data to temp camera object 128 | 129 | # assign action to the player 130 | player.animation_data_create() 131 | player.animation_data.action = action 132 | 133 | # make player the active camera 134 | scene.camera = player 135 | # start from frame one 136 | scene.frame_current = scene.frame_start 137 | # play animation 138 | bpy.ops.screen.animation_play() 139 | 140 | return {'RUNNING_MODAL'} 141 | 142 | def cancel(self, context): 143 | '''cancel animation an remove contraints''' 144 | scene = context.scene 145 | bpy.ops.screen.animation_cancel(restore_frame=True) 146 | # set scene camera back to vp_camera 147 | cam = bpy.data.objects[scene.vp_camera] 148 | cam.hide_viewport = False 149 | scene.camera = cam 150 | return {'FINISHED'} 151 | 152 | 153 | class VP_OT_delete_shot(Operator): 154 | '''Delete Selected Shot''' 155 | bl_idname = "scene.delete_item" 156 | bl_label = "Delete VP Shot" 157 | 158 | @classmethod 159 | def poll(cls, context): 160 | return context.scene.vp_shot_list 161 | 162 | def execute(self, context): 163 | scene = context.scene 164 | index = context.scene.vp_shot_list_index 165 | bpy.data.actions.remove(bpy.data.actions[index]) 166 | 167 | return{'FINISHED'} 168 | 169 | 170 | class VP_OT_use_shot(Operator): 171 | '''Assign selected shot to camera''' 172 | bl_idname = "scene.use_shot" 173 | bl_label = "Delete VP Shot" 174 | 175 | @classmethod 176 | def poll(cls, context): 177 | return (context.scene.vp_shot_list and bpy.data.objects.get(context.scene.scene_camera)) 178 | 179 | def execute(self, context): 180 | scene = context.scene 181 | index = context.scene.vp_shot_list_index 182 | cam = bpy.data.objects[scene.scene_camera] 183 | if cam.animation_data is None: 184 | cam.animation_data_create() 185 | cam.animation_data.action = bpy.data.actions[index] 186 | 187 | return{'FINISHED'} 188 | 189 | 190 | class VIEW_3D_OT_toggle_dof(Operator): 191 | """Toggle depth of field on the VP Camera""" 192 | bl_idname = "scene.toggle_dof" 193 | bl_label = "Toggle Dof" 194 | 195 | @classmethod 196 | def poll(cls,context): 197 | return bpy.data.objects.get(context.scene.vp_camera) 198 | 199 | def execute(self, context): 200 | cam = bpy.data.objects.get(context.scene.vp_camera).data 201 | if not cam.dof.use_dof: 202 | cam.dof.use_dof = True 203 | else: 204 | cam.dof.use_dof = False 205 | return {'FINISHED'} 206 | 207 | 208 | class VIEW_3D_OT_change_focus(Operator): 209 | """Change increase or decrease the focus of the camera""" 210 | bl_idname = "scene.change_focus" 211 | bl_label = "Change VP Focus" 212 | 213 | focus_direction: bpy.props.BoolProperty(default=True) 214 | 215 | @classmethod 216 | def poll(cls,context): 217 | return bpy.data.objects.get(context.scene.vp_camera) 218 | 219 | def execute(self, context): 220 | cam = bpy.data.objects.get(context.scene.vp_camera).data 221 | focus_step = 3 222 | if not self.focus_direction: 223 | focus_step = -focus_step 224 | cam.lens += focus_step 225 | return {'FINISHED'} 226 | 227 | 228 | class VIEW_3D_OT_vp_start_recording(Operator): 229 | """Assign a helper object, start playback and handle recording""" 230 | bl_idname = "scene.vp_start_recording" 231 | bl_label = "Start VP Recorder" 232 | 233 | @classmethod 234 | def poll(cls, context): 235 | return bpy.data.objects.get(context.scene.vp_camera) 236 | 237 | def execute(self, context): 238 | # store autokey 239 | scene = context.scene 240 | scene.frame_current = scene.frame_start 241 | scene.autokeysetting = scene.tool_settings.use_keyframe_insert_auto 242 | scene.tool_settings.use_keyframe_insert_auto = True 243 | 244 | # get the object controlled by the tracker, to get its motion 245 | cam = bpy.data.objects.get(scene.vp_camera) 246 | # make sure the recorded object doesn't have any keyframes, they would interfere with the VR motion 247 | cam.animation_data_clear() 248 | 249 | # assign or create the recorder object 250 | cam_ob = create_recorder_empty(context, "Camera_helper_Empty") 251 | if not scene.vp_action_overwrite: 252 | action = cam_ob.animation_data.action 253 | bpy.ops.scene.add_vp_shot() 254 | else: 255 | cam_ob.animation_data_clear() 256 | 257 | # play and handle recording 258 | bpy.ops.screen.animation_play() 259 | bpy.app.handlers.frame_change_post.append(record_handler) 260 | return {'FINISHED'} 261 | 262 | 263 | class VIEW_3D_OT_vp_stop_recording(Operator): 264 | '''Stop recording and cancel animation''' 265 | bl_idname = "scene.vp_stop_recording" 266 | bl_label = "Stop VP Recorder" 267 | 268 | @classmethod 269 | def poll(cls, context): 270 | return bpy.data.objects.get(context.scene.vp_camera) 271 | 272 | def execute(self, context): 273 | scene = context.scene 274 | cam = bpy.data.objects.get(context.scene.vp_camera) 275 | cam_ob = bpy.data.objects.get("Camera_helper_Empty") 276 | # stop animation and remove handler 277 | bpy.ops.screen.animation_cancel(restore_frame=False) 278 | if record_handler in bpy.app.handlers.frame_change_post: 279 | bpy.app.handlers.frame_change_post.remove(record_handler) 280 | 281 | # set autokey back to what it was 282 | scene.tool_settings.use_keyframe_insert_auto = scene.autokeysetting 283 | return {'FINISHED'} 284 | 285 | 286 | class VIEW_3D_PT_vp_recorder(Panel): 287 | bl_label = "VP Recorder" 288 | bl_space_type = 'VIEW_3D' 289 | bl_region_type = 'UI' 290 | bl_category = 'VR' 291 | bl_context = 'objectmode' 292 | 293 | def draw(self, context): 294 | scene = context.scene 295 | layout = self.layout 296 | layout.use_property_split = True 297 | layout.use_property_decorate = False 298 | layout.prop(scene, "vp_camera") 299 | layout.prop(scene, "scene_camera") 300 | layout.prop(scene, "vp_action_overwrite") 301 | layout.prop(scene, "vp_action_name") 302 | 303 | row = layout.row(align=True) 304 | row.label(text="Focus") 305 | row.operator("scene.change_focus", text="", icon="REMOVE").focus_direction = False 306 | row.operator("scene.change_focus", text="", icon="ADD").focus_direction = True 307 | row = layout.row() 308 | row.operator("scene.toggle_dof") 309 | row = layout.row(align=True) 310 | row.operator("scene.vp_start_recording", text="Start Recording") 311 | row.operator("scene.vp_stop_recording", text="Stop Recording") 312 | 313 | 314 | class VIEW_3D_PT_vp_playback(Panel): 315 | bl_label = "VP Playback" 316 | bl_space_type = 'VIEW_3D' 317 | bl_region_type = 'UI' 318 | bl_category = 'VR' 319 | bl_context = 'objectmode' 320 | 321 | def draw(self, context): 322 | scene = context.scene 323 | layout = self.layout 324 | layout.use_property_split = True 325 | layout.use_property_decorate = False 326 | 327 | row = layout.row() 328 | col = row.column() 329 | ob = context.object 330 | col.template_list("VP_UL_shot_list", "", bpy.data, "actions", scene, "vp_shot_list_index") 331 | col.operator("scene.vp_play_shot") 332 | col.operator("scene.delete_item", text="Remove Shot") 333 | col.operator("scene.use_shot", text="Use Shot") 334 | 335 | 336 | classes = ( 337 | ListItem, 338 | VP_UL_shot_list, 339 | VP_OT_play_shot, 340 | VP_OT_delete_shot, 341 | VP_OT_use_shot, 342 | VP_OT_add_shot, 343 | VIEW_3D_OT_vp_start_recording, 344 | VIEW_3D_PT_vp_playback, 345 | VIEW_3D_OT_toggle_dof, 346 | VIEW_3D_OT_vp_stop_recording, 347 | VIEW_3D_OT_change_focus, 348 | VIEW_3D_PT_vp_recorder 349 | ) 350 | 351 | addon_keymaps = [] 352 | 353 | def register(): 354 | for c in classes: 355 | bpy.utils.register_class(c) 356 | 357 | bpy.types.Scene.vp_action_name = StringProperty( 358 | name="Shot name", 359 | default="shot", 360 | description="Name of the action" 361 | ) 362 | bpy.types.Scene.scene_camera = StringProperty( 363 | name="Scene Camera", 364 | default="Scene_Camera", 365 | description="The camera not controlled by VR, which you can use to preview the shot or use as main scene camera" 366 | ) 367 | bpy.types.Scene.vp_camera = StringProperty( 368 | name="VP Camera", 369 | default="Camera", 370 | description="The camera used in VR, usually driven by the Recorded Object" 371 | ) 372 | bpy.types.Scene.autokeysetting = BoolProperty( 373 | name="Use Autokey" 374 | ) 375 | bpy.types.Scene.vp_action_overwrite = BoolProperty( 376 | name="Overwrite VP action", 377 | default=False, 378 | description="Overwrite VP action or create a new one" 379 | ) 380 | bpy.types.Scene.vp_shot_list_index = IntProperty( 381 | name="Index of Shots", 382 | default=0 383 | ) 384 | bpy.types.Scene.vp_shot_list = CollectionProperty( 385 | type=ListItem 386 | ) 387 | 388 | bpy.app.handlers.frame_change_pre.append(stop_recording) 389 | 390 | # keymap 391 | wm = bpy.context.window_manager 392 | kc = wm.keyconfigs.addon 393 | if kc: 394 | km = kc.keymaps.new(name='3D View', space_type= 'VIEW_3D') 395 | kmi = km.keymap_items.new("scene.vp_start_recording", type='J', value='PRESS') 396 | kmi = km.keymap_items.new("scene.vp_stop_recording", type='L', value='PRESS') 397 | addon_keymaps.append((km, kmi)) 398 | 399 | 400 | def unregister(): 401 | for c in classes: 402 | bpy.utils.unregister_class(c) 403 | 404 | bpy.app.handlers.frame_change_pre.remove(stop_recording) 405 | 406 | for km, kmi in addon_keymaps: 407 | km.keymap_items.remove(kmi) 408 | addon_keymaps.clear() 409 | 410 | del bpy.types.Scene.vp_shot_list 411 | del bpy.types.Scene.vp_shot_list_index 412 | 413 | 414 | if __name__ == "__main__": 415 | register() 416 | -------------------------------------------------------------------------------- /vrais_tools.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 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. 7 | # 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. 12 | # 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. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "VRAIS Tools", 21 | "author": "Sebastian Koenig", 22 | "version": (1,1), 23 | "blender": (2, 77, 0), 24 | "location": "Properties > Render", 25 | "description": "Upload VR Panormas and Cubemaps to vrais.io", 26 | "warning": "", 27 | "wiki_url": "", 28 | "category": "Render" 29 | } 30 | 31 | 32 | import bpy 33 | import os 34 | import http.client 35 | from bpy.props import * 36 | from math import radians 37 | from bpy.types import Operator, AddonPreferences 38 | from bpy.utils import register_class, unregister_class 39 | from addon_utils import check 40 | 41 | 42 | # ########################################################## 43 | # CONFIGURE USER PREFS 44 | # ########################################################## 45 | 46 | class VraisTools(bpy.types.AddonPreferences): 47 | bl_idname = __name__ 48 | 49 | vrais_key = StringProperty(name="VRAIS API KEY") 50 | 51 | def draw(self, context): 52 | layout = self.layout 53 | layout.label(text="Enter your vrais.io API key") 54 | layout.prop(self, "vrais_key") 55 | 56 | 57 | # ########################################################## 58 | # FUNCTIONS 59 | # ########################################################## 60 | 61 | 62 | # define the path of the resulting cubemap 63 | def configure_vrais_cubemap_path(scn): 64 | vs = scn.vrais_settings 65 | suffix = ".jpg" 66 | if vs.filename=="": 67 | vrais_cubemap_path = os.path.join(vs.cube_filepath, os.path.basename(os.path.normpath(vs.source_path)) + suffix) 68 | else: 69 | vrais_cubemap_path = os.path.join(vs.cube_filepath, vs.filename + suffix) 70 | return vrais_cubemap_path 71 | 72 | # create a new temporary scene, which is used to assemble the cubemap stripe from the 12 cubemap tiles. 73 | def create_new_scene(context): 74 | scn = context.scene 75 | size = scn.render.resolution_y 76 | res = scn.render.resolution_percentage 77 | 78 | hashes = [hash(s) for s in bpy.data.scenes] 79 | bpy.ops.scene.new(type='NEW') 80 | new_scn = [s for s in bpy.data.scenes if hash(s) not in hashes][0] 81 | new_scn.name = "vrais_tmp_scene" 82 | 83 | tree = new_scn.node_tree 84 | try: 85 | new_scn.view_settings.view_transform = 'sRGB' 86 | except: 87 | new_scn.view_settings.view_transform = 'Default' 88 | cam_data = scn.camera.data 89 | cam = bpy.data.objects.new("tmp_cam", cam_data) 90 | new_scn.objects.link(cam) 91 | render = new_scn.render 92 | render.resolution_x = size*12 93 | render.resolution_y = size 94 | render.resolution_percentage = res 95 | render.image_settings.file_format = "JPEG" 96 | render.filepath = configure_vrais_cubemap_path(scn) 97 | new_scn.use_nodes = True 98 | for n in new_scn.node_tree.nodes: 99 | new_scn.node_tree.nodes.remove(n) 100 | 101 | return new_scn 102 | 103 | 104 | # create image nodes in the temporary scene 105 | def img_node_creator(new_scn, scn): 106 | tree = new_scn.node_tree 107 | frame = str(scn.frame_current).zfill(4) 108 | file_format = scn.render.image_settings.file_format.lower() 109 | if file_format == "jpeg": # make sure the suffix matches the output 110 | file_format = "jpg" 111 | testlist = [] # this is used for testing if all images are found 112 | # check how the cubemap was generated 113 | # it's enough to know if one of the files in the source folder starts with "NORTH" 114 | type = 0 115 | for root, dirs, files in os.walk(scn.vrais_settings.source_path): 116 | for f in files: 117 | if f.startswith("NORTH"): 118 | type = 1 119 | if type == 1: 120 | img_dict = { 121 | "EAST_%s_R" % (frame):1, 122 | "WEST_%s_R" % (frame):2, 123 | "ZENITH_%s_R" % (frame):3, 124 | "NADIR_%s_R" % (frame):4, 125 | "NORTH_%s_R" % (frame):5, 126 | "SOUTH_%s_R" % (frame):6, 127 | "EAST_%s_L" % (frame):7, 128 | "WEST_%s_L" % (frame):8, 129 | "ZENITH_%s_L" % (frame):9, 130 | "NADIR_%s_L" % (frame):10, 131 | "NORTH_%s_L" % (frame):11, 132 | "SOUTH_%s_L" % (frame):12 133 | } 134 | else: 135 | img_dict = { 136 | "000004_R":1, 137 | "000003_R":2, 138 | "000005_R":3, 139 | "000006_R":4, 140 | "000001_R":5, 141 | "000002_R":6, 142 | "000004_L":7, 143 | "000003_L":8, 144 | "000005_L":9, 145 | "000006_L":10, 146 | "000001_L":11, 147 | "000002_L":12 148 | } 149 | # create new nodes with the images and names from 1 to 12. 150 | # with the node.name we pass the index on to the connector function 151 | for img in img_dict: 152 | img_path = os.path.join(scn.vrais_settings.source_path, img + "." + file_format) 153 | print(img_path) 154 | img_index = img_dict[img] 155 | node = tree.nodes.new(type="CompositorNodeImage") 156 | node.location = node.location[0], node.location[1] - img_index*300 157 | try: 158 | img_new = bpy.data.images.load(filepath=img_path) 159 | node.image = img_new 160 | node.name = str(img_index) 161 | testlist.append(img) 162 | except: 163 | print("there was a problem with the image path") 164 | pathcheck = len(testlist)==12 165 | # the pathcheck will return True only if there are really 12 cubemap tile images 166 | return pathcheck 167 | 168 | 169 | # connect the 12 images with proper offset and size 170 | def connector(new_scn, offset): 171 | tree = new_scn.node_tree 172 | nodes = tree.nodes 173 | links = tree.links 174 | res = new_scn.render.resolution_percentage 175 | offset = offset/100*res 176 | center = -(offset*5)-(offset/2) 177 | 178 | # the first and last node need special treatment... 179 | for i in range(1,12): 180 | img_1 = nodes[str(i)] 181 | img_2 = nodes[str(i+1)] 182 | mix = nodes.new(type="CompositorNodeAlphaOver") 183 | if i == 1: 184 | tl_1 = nodes.new(type="CompositorNodeTransform") 185 | tl_1.location = img_1.location[0] + 100, img_1.location[1] 186 | tl_1.inputs['X'].default_value = center 187 | links.new(img_1.outputs[0], tl_1.inputs[0]) 188 | links.new(tl_1.outputs[0], mix.inputs[1]) 189 | 190 | tl = tree.nodes.new(type="CompositorNodeTransform") 191 | tl.inputs['X'].default_value = center + offset * i 192 | tl.location = img_1.location[0] + 100, img_2.location[1] 193 | mix.location = tl.location[0] + 200, img_2.location[1] + 100 194 | links.new(img_2.outputs[0], tl.inputs[0]) 195 | if not i ==1: 196 | links.new(img_1.outputs[0], mix.inputs[1]) 197 | links.new(tl.outputs[0], mix.inputs[2]) 198 | img_2.name = "old" 199 | mix.name = str(i+1) 200 | 201 | last_node = nodes['12'] 202 | output = nodes.new(type="CompositorNodeComposite") 203 | output.location = last_node.location[0] + 200, last_node.location[1] 204 | links.new(last_node.outputs[0], output.inputs[0]) 205 | 206 | 207 | # upload the vr rendering to vrais.io 208 | def vr_uploader(scn, path): 209 | filepath = path 210 | f = open(filepath, "rb") 211 | chunk = f.read() 212 | f.close() 213 | vs = scn.vrais_settings 214 | if scn.vrais_enum == 'VRAIS_CUBE': 215 | is_cubemap = "1" 216 | else: 217 | is_cubemap = "0" 218 | 219 | headers = { 220 | "Content-type": "multipart/form-data", 221 | "Accept": "text/plain", 222 | "Title": vs.vrais_title, 223 | "Description": vs.description, 224 | "Token": bpy.context.user_preferences.addons[__name__].preferences.vrais_key, 225 | "Convergence": str(scn.camera.data.stereo.convergence_distance), 226 | "IsCubemap": is_cubemap 227 | } 228 | 229 | conn = http.client.HTTPConnection("vrais.io") 230 | conn.request("POST", "/api.php?cmd=uploadItem", chunk, headers) 231 | response = conn.getresponse() 232 | remote_file = response.read() 233 | conn.close() 234 | print ("uploaded ", remote_file) 235 | return str(remote_file) 236 | 237 | 238 | # check if the cubemap addon is enable in User Prefs 239 | def check_cubemap_addon(): 240 | addon = "render_cube_map" 241 | is_enabled, is_loaded = check(addon) 242 | if is_enabled: 243 | return True 244 | 245 | 246 | 247 | # ########################################################## 248 | # OPERATORS 249 | # ########################################################## 250 | 251 | 252 | class VRAIS_OT_setup_cubemap(bpy.types.Operator): 253 | """Setup render and camera parameters for a correct stereoscopic VR cubemap rendering""" 254 | bl_idname = "scene.vrais_setup_cubemap" 255 | bl_label = "Setup Cubemap" 256 | 257 | def execute(self, context): 258 | scn = context.scene 259 | render = scn.render 260 | cam_data = scn.camera.data 261 | 262 | render.engine = 'CYCLES' 263 | render.resolution_y = 1280 # recommended resolution for VRAIS 264 | render.resolution_x = render.resolution_y 265 | render.use_multiview = True 266 | render.image_settings.views_format = 'INDIVIDUAL' 267 | 268 | cam_data.type = 'PERSP' 269 | cam_data.angle = radians(90) 270 | 271 | try: 272 | cam_data.stereo.use_spherical_stereo = True 273 | except: 274 | self.report( 275 | {'ERROR'}, 276 | "You seem to be using Blender 2.77 or lower, which does not support Spherical Stereo. Please consider upgrading to at least 2.78." 277 | ) 278 | 279 | scn.vrais_enum = 'VRAIS_CUBE' 280 | 281 | return {'FINISHED'} 282 | 283 | 284 | 285 | class VRAIS_OT_setup_vr_panorama(bpy.types.Operator): 286 | """Setup render and camera parameters for a correct stereoscopic VR panorama rendering""" 287 | bl_idname = "scene.vrais_setup_vr_panorama" 288 | bl_label = "Setup VR Panorama" 289 | 290 | def execute(self, context): 291 | scn = context.scene 292 | render = scn.render 293 | cam_data = scn.camera.data 294 | 295 | 296 | render.engine = 'CYCLES' 297 | render.resolution_y = 2048 # recommended resolution for VRAIS 298 | render.resolution_x = render.resolution_y * 2 299 | render.use_multiview = True 300 | render.image_settings.views_format = 'STEREO_3D' 301 | render.image_settings.stereo_3d_format.display_mode = "TOPBOTTOM" 302 | 303 | cam_data.cycles.panorama_type = 'EQUIRECTANGULAR' 304 | cam_data.type = 'PANO' 305 | 306 | try: 307 | cam_data.stereo.use_spherical_stereo = True 308 | except: 309 | self.report( 310 | {'ERROR'}, 311 | "You seem to be using Blender 2.77 or lower, which does not support Spherical Stereo. Please consider upgrading to at least 2.78." 312 | ) 313 | 314 | scn.vrais_enum = 'VRAIS_EQUI' 315 | scn.vrais_settings.equi_filepath = render.filepath 316 | 317 | return {'FINISHED'} 318 | 319 | 320 | 321 | class VRAIS_OT_create_cubemap(bpy.types.Operator): 322 | """Create the cubemap stripe""" 323 | bl_idname = "scene.vrais_create_cubemap" 324 | bl_label = "Create Cubemap Stripe" 325 | 326 | def execute(self, context): 327 | scn = context.scene 328 | screen = context.screen 329 | new_scn = create_new_scene(context) 330 | if img_node_creator(new_scn, scn): 331 | connector(new_scn, scn.render.resolution_y) 332 | bpy.ops.render.render(write_still=True, scene = new_scn.name) 333 | else: 334 | self.report( 335 | {'ERROR'}, 336 | "Couldn't load the cubemap tiles. Please check the render filepath." 337 | ) 338 | bpy.data.scenes.remove(bpy.data.scenes[new_scn.name], do_unlink=True) 339 | context.screen.scene=bpy.data.scenes[scn.name] 340 | 341 | return {'FINISHED'} 342 | 343 | 344 | 345 | class VRAIS_OT_uploader(bpy.types.Operator): 346 | """Upload to vrais.io""" 347 | bl_idname = "scene.vrais_uploader" 348 | bl_label = "Upload to vrais.io" 349 | 350 | def execute(self, context): 351 | scn = context.scene 352 | vs = scn.vrais_settings 353 | 354 | # check whether to upload panorama or cubemap 355 | if scn.vrais_enum == 'VRAIS_CUBE': 356 | path = bpy.path.abspath(configure_vrais_cubemap_path(scn)) 357 | elif scn.vrais_enum == 'VRAIS_EQUI': 358 | path = bpy.path.abspath(vs.equi_filepath) 359 | 360 | # check if camera, title and description are setup correctly 361 | if not scn.camera.data.stereo.use_spherical_stereo: 362 | self.report( 363 | {'ERROR'}, 364 | "Spherical Stereo was not enabled in camera settings. \ 365 | This will lead to wrong stereo effect!" 366 | ) 367 | return {'CANCELLED'} 368 | elif len(vs.description)==0: 369 | self.report( 370 | {'ERROR'}, 371 | "Please fill in a description" 372 | ) 373 | return {'CANCELLED'} 374 | elif len(vs.vrais_title)==0: 375 | self.report( 376 | {'ERROR'}, 377 | "Please fill in a Title" 378 | ) 379 | return {'CANCELLED'} 380 | elif len(context.user_preferences.addons[__name__].preferences.vrais_key)<2: 381 | self.report( 382 | {'ERROR'}, 383 | "No VRAIS API key configured in Addon Preferences!" 384 | ) 385 | return {'CANCELLED'} 386 | else: 387 | # if all is fine, upload the VR rendering 388 | self.report( 389 | {'INFO'}, 390 | vr_uploader(scn, path) 391 | ) 392 | 393 | return {'FINISHED'} 394 | 395 | 396 | 397 | # ########################################################## 398 | # UI 399 | ## ########################################################## 400 | 401 | 402 | class RENDER_PT_vrais_tools(bpy.types.Panel): 403 | """Configure the VRAIS tools UI""" 404 | bl_idname = "scene.vrais_tools" 405 | bl_label = "VRAIS Tools" 406 | bl_space_type = 'PROPERTIES' 407 | bl_region_type = 'WINDOW' 408 | bl_context = "render" 409 | 410 | def draw(self, context): 411 | layout = self.layout 412 | scn = context.scene 413 | vs = scn.vrais_settings 414 | cubemap = scn.vrais_enum == 'VRAIS_CUBE' 415 | 416 | layout.label(text="Setup your VR Rendering") 417 | row = layout.row(align=True) 418 | row.operator("scene.vrais_setup_cubemap") 419 | row.operator("scene.vrais_setup_vr_panorama") 420 | 421 | layout.label(text="VRAIS upload configuration") 422 | col = layout.column() 423 | col.prop(scn, "vrais_enum", text="VR Type") 424 | col.prop(vs, 'vrais_title') 425 | col.prop(vs, 'description') 426 | if cubemap: 427 | col.prop(vs, 'source_path') 428 | col.prop(vs, 'cube_filepath') 429 | col.prop(vs, 'filename') 430 | col.operator("scene.vrais_create_cubemap", icon="IMAGE_DATA") 431 | col.operator("scene.vrais_uploader", text="Upload Cubemap", icon="FILE_TICK") 432 | else: 433 | col.prop(vs, 'equi_filepath') 434 | col.operator("scene.vrais_uploader", text="Upload VR Panorama", icon="FILE_TICK") 435 | 436 | 437 | 438 | # ########################################################## 439 | # PROPERTIES 440 | # ########################################################## 441 | 442 | 443 | class VraisSettings(bpy.types.PropertyGroup): 444 | vrais_title = StringProperty( 445 | name="Image Title", 446 | description="You need to fill in a title for the image to be displayed in VRAIS") 447 | description = StringProperty( 448 | name="Scene Description", 449 | description="You need to fill in a description for the image to be displayed in VRAIS") 450 | filename = StringProperty( 451 | name="Name", 452 | description="Set the name of the Cubemap Stripe, which will be generated and uploaded to VRAIS", 453 | default="") 454 | equi_filepath = StringProperty( 455 | name="Filepath", 456 | description="Choose the VR Panorama File", 457 | subtype='FILE_PATH') 458 | source_path = StringProperty( 459 | name="Cubemap Source", 460 | description="Choose the folder with the source images", 461 | subtype='DIR_PATH') 462 | cube_filepath = StringProperty( 463 | name="Cubemap Target", 464 | description="Here's where the Cubemap will be generated (and loaded)", 465 | subtype='DIR_PATH', 466 | default="//../") 467 | 468 | 469 | 470 | # ########################################################## 471 | # REGISTER 472 | # ########################################################## 473 | 474 | 475 | classes = ( 476 | VraisTools, 477 | VraisSettings, 478 | VRAIS_OT_uploader, 479 | VRAIS_OT_setup_cubemap, 480 | VRAIS_OT_create_cubemap, 481 | VRAIS_OT_setup_vr_panorama, 482 | RENDER_PT_vrais_tools 483 | ) 484 | 485 | def register(): 486 | for c in classes: 487 | register_class(c) 488 | 489 | bpy.types.Scene.vrais_settings = PointerProperty(type=VraisSettings) 490 | bpy.types.Scene.vrais_enum = bpy.props.EnumProperty( 491 | items=( 492 | ('VRAIS_CUBE','Cubemap','Cubemap Stripe', 1), 493 | ('VRAIS_EQUI','Equirectangular','Equirectangular Rendering', 2), 494 | ) 495 | ) 496 | 497 | def unregister(): 498 | for c in classes: 499 | unregister_class(c) 500 | 501 | del bpy.types.Scene.vrais_settings 502 | 503 | if __name__ == "__main__": 504 | register() 505 | --------------------------------------------------------------------------------