├── 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 |
--------------------------------------------------------------------------------