├── README.md └── dof-utils.py /README.md: -------------------------------------------------------------------------------- 1 | ### Depth of Field Utilities 2 | 3 | Prototype to visualize the [Depth of Field](https://en.wikipedia.org/wiki/Depth_of_field) limits in the *3d Viewport*. 4 | 5 | ![intro](https://i.sstatic.net/jYyag.jpg) 6 | 7 | In addition to that, the Add-on also allows to set the [focus distance](https://docs.blender.org/manual/en/latest/render/cameras.html#depth-of-field) by using the *3d Cursor*. 8 | 9 | #### Installation 10 | 11 | 1. Download the [latest release](https://github.com/p2or/blender-dof-utils/releases) 12 | 2. In Blender open up *User Preferences > Addons* 13 | 3. Click *Install from File*, select `dof-utils.py` and activate the Add-on 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /dof-utils.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": "Depth of Field Utilities", 21 | "author": "Christian Brinkmann (p2or)", 22 | "description": "Displays depth of field in 3D viewport.", 23 | "version": (0, 1, 3), 24 | "blender": (4, 0, 0), 25 | "location": "3d View > Properties Panel (N) > Depth of Field Utilities", 26 | "doc_url": "https://github.com/p2or/blender-dof-utils", 27 | "tracker_url": "https://github.com/p2or/blender-dof-utils/issues", 28 | "category": "Render" 29 | } 30 | 31 | import bpy 32 | import blf 33 | import gpu 34 | from gpu_extras.batch import batch_for_shader 35 | import math 36 | from mathutils import Matrix, Vector 37 | from mathutils.geometry import intersect_point_line 38 | 39 | 40 | # ------------------------------------------------------------------------ 41 | # Preferences & Scene Properties 42 | # ------------------------------------------------------------------------ 43 | 44 | class DOFU_AP_preferences(bpy.types.AddonPreferences): 45 | 46 | bl_idname = __name__ 47 | 48 | display_info: bpy.props.BoolProperty( 49 | name="Display Infos in Viewport", 50 | default = True) 51 | 52 | display_limits: bpy.props.BoolProperty( 53 | name="Display Limits in Viewport Header", 54 | description="Displays distance, near & far limits in viewport header", 55 | default = True) 56 | 57 | def draw(self, context): 58 | layout = self.layout 59 | row = layout.row(align=True) 60 | row.prop(self, "display_info") 61 | row.prop(self, "display_limits") 62 | layout.row().operator("dof_utils.reset_preferences", icon='FILE_REFRESH') 63 | 64 | 65 | class DOFU_PG_settings(bpy.types.PropertyGroup): 66 | 67 | _visualize_handle = None 68 | _instructions_handle = None 69 | 70 | use_cursor: bpy.props.BoolProperty( 71 | name="Use 3d Cursor Flag", 72 | description="", 73 | default=False, 74 | options={'SKIP_SAVE'}) 75 | 76 | draw_dof: bpy.props.BoolProperty( 77 | name="Draw DoF Flag", 78 | description="", 79 | default=False, 80 | options={'SKIP_SAVE'}) 81 | 82 | overlay: bpy.props.BoolProperty( 83 | name="Line overlay", 84 | description="Display DoF above all other Elements", 85 | default=True) 86 | 87 | size_limits: bpy.props.FloatProperty( 88 | name="Size", 89 | description="Limit Radius", 90 | min=0.0, 91 | step=1, 92 | default=0.1) 93 | 94 | fill_limits: bpy.props.BoolProperty( 95 | name="Fill limits", 96 | description="Fill Limits", 97 | default=False) 98 | 99 | draw_focus: bpy.props.BoolProperty( 100 | name="Display Focus", 101 | description="Draw Focus", 102 | default=False) 103 | 104 | color_limits: bpy.props.FloatVectorProperty( 105 | name="Color Limits", 106 | subtype='COLOR', 107 | default=(0.0, 1.0, 0.0), 108 | min=0.0, max=1.0, 109 | description="color picker") 110 | 111 | segments_limits: bpy.props.IntProperty( 112 | name="Segments", 113 | default=16, 114 | min=3, max=32) 115 | 116 | opacity_limits: bpy.props.FloatProperty( 117 | name="Opacity", 118 | min=0.1, max=1.0, 119 | step=1, 120 | default=0.9) 121 | 122 | limits: bpy.props.FloatVectorProperty( 123 | name="Limits", 124 | size=3) 125 | 126 | # ------------------------------------------------------------------------ 127 | # Helper 128 | # ------------------------------------------------------------------------ 129 | 130 | def is_camera(obj): 131 | return obj is not None and obj.type == 'CAMERA' 132 | 133 | def distance(pt1, pt2): 134 | pt_apex, pt_base = sorted((pt1, pt2), key=lambda v: v.z, reverse=True) 135 | return (Vector((pt_apex.x, pt_apex.y, pt_base.z)) - pt_base).length 136 | 137 | def fstops(camera_data, magnification): 138 | """ Return or calculate f-stops (N) """ 139 | # Following lines rem as radius option not exist 140 | #if camera_data.cycles.aperture_type == 'RADIUS': 141 | # #print("Radius:", ((cam.lens / cam.cycles.aperture_fstop) / 2000)) 142 | # if camera_data.cycles.aperture_size > 0.00001: # division by zero fix 143 | # return ((camera_data.lens /1000 * magnification) / (camera_data.cycles.aperture_size *2)) 144 | # else: 145 | # return camera_data.clip_end 146 | #else: 147 | return camera_data.dof.aperture_fstop #.cycles.aperture_fstop 148 | 149 | def dof_calculation(camera_data, dof_distance, magnification=1): 150 | # https://en.wikipedia.org/wiki/Depth_of_focus#Calculation 151 | m = 1 # Magnification 152 | f = 22 / 1000 * m # Focal length and unit conversation 153 | N = 1.7817975283 # Lens f number (1.8) 154 | c = 0.032 # Circle of Confusion (default full frame) 155 | d = 3 # Subject distance 156 | 157 | N = fstops(camera_data, magnification) 158 | f = camera_data.lens / 1000 * magnification 159 | 160 | # Calculate Circle of confusion (diameter limit based on d/1500) 161 | # https://en.wikipedia.org/wiki/Circle_of_confusion#Circle_of_confusion_diameter_limit_based_on_d.2F1500 162 | c = math.sqrt(camera_data.sensor_width**2 + camera_data.sensor_height**2) / 1500 163 | 164 | # Hyperfocal distance (H) 165 | # https://en.wikipedia.org/wiki/Hyperfocal_distance#Formulae 166 | a = math.pow(f, 2) / (N * c * magnification / 1000) # respect the units 167 | H = a + f 168 | 169 | Hn = H / 2 # Hyperfocal near limit 170 | nL = (a * dof_distance) / (a + (dof_distance - f)) # DoF near limit 171 | if (0.01 > (H - dof_distance)): # DoF far limit 172 | fL = camera_data.clip_end # math.inf 173 | else: 174 | fL = (a * dof_distance) / (a - (dof_distance - f)) 175 | DoF = fL - nL # Depth of field 176 | DoFf = dof_distance - nL # Depth in Front 177 | Dofb = fL - dof_distance # Depth Behind 178 | return (nL, fL) 179 | 180 | 181 | # ------------------------------------------------------------------------ 182 | # OpenGL callbacks 183 | # ------------------------------------------------------------------------ 184 | 185 | def draw_callback_3d(operator, context): 186 | 187 | scn = context.scene 188 | dofu = scn.dof_utils 189 | 190 | if is_camera(context.object): 191 | cam_ob = context.object 192 | elif is_camera(scn.camera): 193 | cam_ob = scn.camera 194 | else: 195 | return 196 | 197 | mat = cam_ob.matrix_world 198 | cam = cam_ob.data 199 | nmat = mat.normalized() 200 | target_scale = (1, 1, 1) 201 | smat = Matrix() 202 | for i in range(3): 203 | smat[i][i] = target_scale[i] 204 | temp_matrix = nmat @ smat # cam_ob.matrix_world = nmat * smat 205 | 206 | start = temp_matrix @ Vector((0, 0, -cam.clip_start)) 207 | end = temp_matrix @ Vector((0, 0, -cam.clip_end)) 208 | d = cam.dof.focus_distance 209 | 210 | if cam.dof.focus_object is None: 211 | near_limit, far_limit = dof_calculation(cam, d) 212 | dof_loc = temp_matrix @ Vector((0, 0, -(near_limit))) 213 | dof_loc_end = temp_matrix @ Vector((0, 0, -(far_limit))) 214 | 215 | else: 216 | pt = cam.dof.focus_object.matrix_world.translation 217 | loc = intersect_point_line(pt, temp_matrix.translation, temp_matrix @ Vector((0, 0, -1))) 218 | d = ((loc[0] - start).length) + cam.clip_start # respect the clipping start value 219 | 220 | near_limit, far_limit = dof_calculation(cam, d) 221 | dof_loc = temp_matrix @ Vector((0, 0, -(near_limit))) 222 | dof_loc_end = temp_matrix @ Vector((0, 0, -(far_limit))) 223 | 224 | dofu.limits = (d, near_limit, far_limit) 225 | 226 | # 80% alpha, 2 pixel width line 227 | gpu.state.blend_set('ALPHA') # bgl.glEnable(bgl.GL_BLEND) 228 | #bgl.glEnable(bgl.GL_LINE_SMOOTH) # -> No replacement gpu.shader.from_builtin('POLYLINE_SMOOTH_COLOR') 229 | gpu.state.depth_test_set("LESS") # bgl.glEnable(bgl.GL_DEPTH_TEST) 230 | 231 | # check overlay 232 | if dofu.overlay: 233 | gpu.state.depth_test_set("NONE") # bgl.glDisable(bgl.GL_DEPTH_TEST) 234 | else: 235 | gpu.state.depth_test_set("LESS") # bgl.glEnable(bgl.GL_DEPTH_TEST) 236 | 237 | # set line width 238 | gpu.state.line_width_set(2) # bgl.glLineWidth(2) 239 | 240 | def line(color, start, end): 241 | vertices = [start,end] 242 | shader = gpu.shader.from_builtin('UNIFORM_COLOR') 243 | batch = batch_for_shader(shader,'LINE_STRIP', {"pos": vertices}) 244 | shader.bind() 245 | shader.uniform_float("color", color) 246 | batch.draw(shader) 247 | #bgl.glColor4f(*color) 248 | #bgl.glBegin(bgl.GL_LINES) 249 | #bgl.glVertex3f(*start) 250 | #bgl.glVertex3f(*end) 251 | #bgl.glEnd() 252 | 253 | # define the lines 254 | line((1.0, 1.0, 1.0, 0.1), dof_loc_end, end) 255 | line((1.0, 1.0, 1.0, 0.1), dof_loc, start) 256 | line((dofu.color_limits[0], dofu.color_limits[1], dofu.color_limits[2], dofu.opacity_limits), dof_loc_end, dof_loc) 257 | 258 | if dofu.size_limits > 0.0: 259 | #draw_empty(matrix=temp_matrix, offset=-near_limit, size=1) 260 | for i in [near_limit, far_limit]: 261 | draw_circle( 262 | matrix=temp_matrix, 263 | offset=-i, 264 | color=(dofu.color_limits[0], dofu.color_limits[1], dofu.color_limits[2], dofu.opacity_limits), 265 | radius=dofu.size_limits, 266 | fill=dofu.fill_limits, 267 | num_segments=dofu.segments_limits) 268 | 269 | if dofu.draw_focus: 270 | draw_empty_2d( 271 | matrix=temp_matrix, 272 | offset=-d, 273 | size=dofu.size_limits * 1.7, 274 | color=(dofu.color_limits[0], dofu.color_limits[1], dofu.color_limits[2], dofu.opacity_limits)) 275 | 276 | # restore defaults 277 | gpu.state.line_width_set(1.0) 278 | gpu.state.blend_set('NONE') 279 | 280 | def draw_string(x, y, packed_strings): 281 | font_id = 0 282 | blf.size(font_id, 17*(bpy.context.preferences.system.dpi/72)) 283 | x_offset = 0 284 | for pstr, pcol in packed_strings: 285 | text_width, text_height = blf.dimensions(font_id, pstr) 286 | blf.position(font_id, (x + x_offset), y, 0) 287 | blf.color(font_id, *pcol) 288 | blf.draw(font_id, pstr) 289 | x_offset += text_width 290 | 291 | 292 | def draw_callback_2d(operator, context): 293 | x, y = (70, 30) 294 | WHITE = (1, 1, 1, 1) 295 | GREEN = (0, 1, 0, 1) 296 | BLUE = (0, 0, 1, 1) 297 | 298 | ps=[("Hit ", WHITE), 299 | ("ESC ", GREEN), 300 | ("or ", WHITE), 301 | ("RMB ", GREEN), 302 | ("when done", WHITE) 303 | ] 304 | draw_string(x, y, ps) 305 | 306 | def draw_poly(coords, color, width): 307 | # Get shader 308 | shader = gpu.shader.from_builtin('UNIFORM_COLOR') 309 | # Create batch process 310 | batch = batch_for_shader(shader,'LINE_STRIP', {"pos": coords}) 311 | # Set the line width 312 | gpu.state.line_width_set(width) # bgl.glLineWidth(width) 313 | shader.bind() 314 | # Set color 315 | shader.uniform_float("color",color) 316 | # Draw line 317 | batch.draw(shader) 318 | 319 | 320 | # Draws a line on the view port being two points 321 | def draw_line_3d(start, end, color=None, width=1): 322 | # Use default color or color given if possible 323 | color = (0.0, 0.0, 0.0, 1.0) if color is None else color 324 | draw_poly([start,end], color, width) 325 | 326 | ''' 327 | def draw_empty(matrix, size, offset=0, offset_axis="Z", color=None, width=1): 328 | vector_list = [ 329 | Vector((size, 0, 0)), Vector((-size, 0, 0)), # x 330 | Vector((0, size, 0)), Vector((0, -size, 0)), # y 331 | Vector((0, 0, size)), Vector((0, 0, -size))] # z 332 | 333 | translate = { 334 | 'X': Vector((offset, 0, 0)), 335 | 'Y': Vector((0, offset, 0)), 336 | 'Z': Vector((0, 0, offset))} 337 | 338 | origin = matrix * translate[offset_axis] #origin = matrix * Vector((0, 0, 0)) 339 | for v in vector_list: 340 | end = matrix * (v + translate[offset_axis]) 341 | draw_line_3d(origin, end) 342 | ''' 343 | 344 | def draw_empty_2d(matrix, size, offset=0, offset_axis="Z", color=None, width=1): 345 | vector_list = [ 346 | Vector((size, 0, 0)), Vector((-size, 0, 0)), # x 347 | Vector((0, size, 0)), Vector((0, -size, 0))] # y 348 | 349 | translate = { 350 | 'X': Vector((offset, 0, 0)), 351 | 'Y': Vector((0, offset, 0)), 352 | 'Z': Vector((0, 0, offset))} 353 | 354 | origin = matrix @ translate[offset_axis] #origin = matrix * Vector((0, 0, 0)) 355 | for v in vector_list: 356 | end = matrix @ (v + translate[offset_axis]) 357 | draw_line_3d(origin, end) 358 | 359 | # based on http://slabode.exofire.net/circle_draw.shtml 360 | def draw_circle(matrix, radius=.1, num_segments=16, offset=0, offset_axis="Z", color=None, width=1, fill=False): 361 | #precalculate the sine and cosine 362 | theta = 2 * math.pi / num_segments 363 | c = math.cos(theta) 364 | s = math.sin(theta) 365 | x = radius 366 | y = 0 367 | 368 | vector_list = [] 369 | for i in range (num_segments+1): 370 | vector_list.append(Vector((x, y, 0))) # output vertex 371 | t = x 372 | x = c * x - s * y 373 | y = s * t + c * y 374 | 375 | translate = { 376 | 'X': Vector((offset, 0, 0)), 377 | 'Y': Vector((0, offset, 0)), 378 | 'Z': Vector((0, 0, offset))} 379 | 380 | #if not fill: # bgl.GL_TRIANGLE_FAN, http://www.glprogramming.com/red/chapter02.html 381 | # bgl.glBegin(bgl.GL_LINE_LOOP) 382 | #else: 383 | # bgl.glBegin(bgl.GL_TRIANGLE_FAN) 384 | draw_points = [] 385 | for v in vector_list: 386 | coord = matrix @ (v + translate[offset_axis]) 387 | draw_points.append(coord) 388 | 389 | draw_poly(draw_points, color, width) 390 | 391 | 392 | # ------------------------------------------------------------------------ 393 | # Operators 394 | # ------------------------------------------------------------------------ 395 | 396 | class DOFU_OT_focusPicking(bpy.types.Operator): 397 | """Sets the focus distance by using the 3d cursor""" 398 | bl_idname = "dof_utils.focus_picking" 399 | bl_label = "Set Focus using 3d Cursor" 400 | bl_description = "Sets the focus distance by using the 3d cursor" 401 | bl_options = {'REGISTER', 'UNDO'} 402 | 403 | _tool = None 404 | 405 | @classmethod 406 | def poll(cls, context): 407 | return is_camera(context.object) 408 | 409 | def redraw_viewports(self, context): 410 | for area in context.screen.areas: 411 | if area.type == 'VIEW_3D': 412 | area.tag_redraw() 413 | 414 | def modal(self, context, event): 415 | scene = context.scene 416 | dofu = scene.dof_utils 417 | prefs = context.preferences.addons[__name__].preferences 418 | 419 | if context.area is not None: 420 | context.area.tag_redraw() 421 | 422 | try: 423 | # Set cursor tool 424 | bpy.ops.wm.tool_set_by_id(name ="builtin.cursor") 425 | except: 426 | bpy.types.SpaceView3D.draw_handler_remove(DOFU_PG_settings._instructions_handle, 'WINDOW') 427 | DOFU_PG_settings._instructions_handle = None 428 | context.scene.dof_utils.use_cursor = False 429 | return {'CANCELLED'} 430 | 431 | if event.type == 'LEFTMOUSE': 432 | if event.value == 'RELEASE': 433 | context.object.data.dof.focus_distance = \ 434 | distance(scene.cursor.location, context.object.matrix_world.to_translation()) 435 | return {'PASS_THROUGH'} 436 | 437 | elif event.type in {'RIGHTMOUSE', 'ESC'} or not dofu.use_cursor: 438 | dofu.use_cursor = False 439 | try: 440 | bpy.types.SpaceView3D.draw_handler_remove(DOFU_PG_settings._instructions_handle, 'WINDOW') 441 | DOFU_PG_settings._instructions_handle = None 442 | except: 443 | pass 444 | # Reset to selected tool before running the operator 445 | bpy.ops.wm.tool_set_by_id(name=self._tool) 446 | self.redraw_viewports(context) 447 | return {'CANCELLED'} 448 | 449 | return {'PASS_THROUGH'} 450 | 451 | def invoke(self, context, event): 452 | dofu = context.scene.dof_utils #context.area.tag_redraw() 453 | prefs = context.preferences.addons[__name__].preferences 454 | 455 | if not dofu.use_cursor: 456 | if context.area.type == 'VIEW_3D': 457 | 458 | # Get the current active tool 459 | from bl_ui.space_toolsystem_common import ToolSelectPanelHelper 460 | self._tool = ToolSelectPanelHelper.tool_active_from_context(context).idname 461 | 462 | if prefs.display_info and not DOFU_PG_settings._instructions_handle: 463 | args = (self, context) 464 | DOFU_PG_settings._instructions_handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_2d, args, 'WINDOW', 'POST_PIXEL') 465 | 466 | context.window_manager.modal_handler_add(self) 467 | dofu.use_cursor = True 468 | return {'RUNNING_MODAL'} 469 | else: 470 | self.report({'WARNING'}, "View3D not found, cannot run operator") 471 | return {'CANCELLED'} 472 | else: 473 | self.report({'WARNING'}, "Operator is already running") 474 | return {'CANCELLED'} 475 | 476 | 477 | class DOFU_OT_visualizeLimits(bpy.types.Operator): 478 | """ Draws depth of field in the viewport via OpenGL """ 479 | bl_idname = "dof_utils.visualize_dof" 480 | bl_label = "Visualize Depth of Field" 481 | bl_description = "Draws depth of field in the vieport via OpenGL" 482 | 483 | @classmethod 484 | def poll(cls, context): 485 | #rd = context.scene.render 486 | return is_camera(context.object) #and rd.engine == "CYCLES" 487 | 488 | def redraw_viewports(self, context): 489 | for area in context.screen.areas: 490 | if area.type == 'VIEW_3D': 491 | area.tag_redraw() 492 | 493 | def modal(self, context, event): 494 | dofu = context.scene.dof_utils 495 | prefs = context.preferences.addons[__name__].preferences 496 | 497 | if context.area is not None: 498 | context.area.tag_redraw() 499 | 500 | if prefs.display_limits and context.area is not None: 501 | context.area.header_text_set("Focus Distance: %.3f Near Limit: %.3f Far Limit: %.3f" % tuple(dofu.limits)) 502 | 503 | if event.type in {'RIGHTMOUSE', 'ESC'} or not dofu.draw_dof: 504 | dofu.draw_dof = False 505 | try: # TODO, viewport class 506 | bpy.types.SpaceView3D.draw_handler_remove(DOFU_PG_settings._visualize_handle, 'WINDOW') 507 | bpy.types.SpaceView3D.draw_handler_remove(DOFU_PG_settings._instructions_handle, 'WINDOW') 508 | DOFU_PG_settings._instructions_handle = None 509 | DOFU_PG_settings._visualize_handle = None 510 | except: 511 | pass 512 | if context.area is not None: 513 | context.area.header_text_set(text=None) 514 | self.redraw_viewports(context) 515 | return {'CANCELLED'} 516 | 517 | return {'PASS_THROUGH'} 518 | 519 | def invoke(self, context, event): 520 | dofu = context.scene.dof_utils 521 | prefs = context.preferences.addons[__name__].preferences 522 | 523 | if not dofu.draw_dof: 524 | if context.area.type == 'VIEW_3D': 525 | args = (self, context) 526 | # Add the region OpenGL drawing callback, draw in view space with 'POST_VIEW' and 'PRE_VIEW' 527 | DOFU_PG_settings._visualize_handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d, args, 'WINDOW', 'POST_VIEW') 528 | 529 | if prefs.display_info and not DOFU_PG_settings._instructions_handle: 530 | DOFU_PG_settings._instructions_handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_2d, args, 'WINDOW', 'POST_PIXEL') 531 | 532 | context.window_manager.modal_handler_add(self) 533 | dofu.draw_dof = True 534 | self.redraw_viewports(context) 535 | return {'RUNNING_MODAL'} 536 | 537 | else: 538 | self.report({'WARNING'}, "View3D not found, cannot run operator") 539 | return {'CANCELLED'} 540 | else: 541 | self.report({'WARNING'}, "Operator is already running") 542 | return {'CANCELLED'} 543 | 544 | 545 | class DOFU_OT_killVisualization(bpy.types.Operator): 546 | """ Kill Visualization """ 547 | bl_idname = "dof_utils.kill_visualization" 548 | bl_label = "Kill Visualization" 549 | bl_description = "Kills Viewport Visualization" 550 | 551 | def execute(self, context): 552 | context.scene.dof_utils.draw_dof = False 553 | return {'FINISHED'} 554 | 555 | 556 | class DOFU_OT_killFocusPicking(bpy.types.Operator): 557 | """ Kill Focus Picking """ 558 | bl_idname = "dof_utils.kill_focus_picking" 559 | bl_label = "Kill Visualization" 560 | bl_description = "Kills Focus Picking" 561 | 562 | def execute(self, context): 563 | context.scene.dof_utils.use_cursor = False 564 | return {'FINISHED'} 565 | 566 | 567 | class DOFU_OT_viewportReset(bpy.types.Operator): 568 | """ Reset Viewport """ 569 | bl_idname = "dof_utils.reset_viewport" 570 | bl_label = "Reset Viewport" 571 | bl_options = {"INTERNAL"} 572 | 573 | # TODO, viewport class 574 | def execute(self, context): 575 | try: 576 | DOFU_PG_settings._instructions_handle = None 577 | DOFU_PG_settings._visualize_handle = None 578 | bpy.types.SpaceView3D.draw_handler_remove(DOFU_PG_settings._instructions_handle, 'WINDOW') 579 | bpy.types.SpaceView3D.draw_handler_remove(DOFU_PG_settings._visualize_handle, 'WINDOW') 580 | 581 | except: 582 | pass 583 | return {'FINISHED'} 584 | 585 | 586 | class DOFU_OT_preferencesReset(bpy.types.Operator): 587 | """ Reset Add-on Preferences """ 588 | bl_idname = "dof_utils.reset_preferences" 589 | bl_label = "Reset Properties and Settings" 590 | bl_options = {"INTERNAL"} 591 | 592 | def execute(self, context): 593 | scn = context.scene 594 | dofu = scn.dof_utils 595 | prefs = context.preferences.addons[__name__].preferences 596 | prefs.property_unset("display_info") 597 | prefs.property_unset("display_limits") 598 | dofu.property_unset("use_cursor") 599 | dofu.property_unset("draw_dof") 600 | dofu.property_unset("overlay") 601 | dofu.property_unset("limits") 602 | bpy.ops.wm.save_userpref() 603 | bpy.ops.dof_utils.reset_viewport() 604 | return {'FINISHED'} 605 | 606 | 607 | # ------------------------------------------------------------------------ 608 | # UI 609 | # ------------------------------------------------------------------------ 610 | 611 | class DOFU_panel: 612 | bl_space_type = "VIEW_3D" 613 | bl_region_type = "UI" 614 | bl_category = "DoF Utils" 615 | 616 | @classmethod 617 | def poll(cls, context): 618 | return is_camera(context.object) 619 | 620 | 621 | class DOFU_PT_main_panel(DOFU_panel, bpy.types.Panel): 622 | bl_label = "Depth of Field" 623 | bl_category = "DoF Utils" 624 | 625 | def draw_header(self, context): 626 | self.layout.prop(context.active_object.data.dof, "use_dof", text="") 627 | 628 | def draw(self, context): 629 | dofu = context.scene.dof_utils #cam_ob = scene.camera.data 630 | cam_ob = context.active_object.data 631 | 632 | layout = self.layout 633 | layout.use_property_split = True 634 | 635 | layout.active = cam_ob.dof.use_dof 636 | active_flag = not dofu.use_cursor and cam_ob.dof.focus_object is None 637 | 638 | # Visualize 639 | row = layout.row(align=True) 640 | viz = row.column(align=True) 641 | viz.enabled = not dofu.draw_dof # enabled 642 | viz.operator("dof_utils.visualize_dof", icon="SNAP_NORMAL" if not dofu.draw_dof else "REC") 643 | row = row.column(align=True) 644 | row.operator("dof_utils.kill_visualization", icon="X", text="") 645 | 646 | # Focus Picking 647 | row = layout.row(align=True) 648 | pic = row.column(align=True) 649 | pic.enabled = active_flag # enabled 650 | pic.operator("dof_utils.focus_picking", icon="RESTRICT_SELECT_OFF" if active_flag or cam_ob.dof.focus_object else "REC") 651 | row = row.column(align=True) #layout.prop_search(dofu, "camera", bpy.data, "cameras") 652 | row.enabled = cam_ob.dof.focus_object is None 653 | row.operator("dof_utils.kill_focus_picking", icon="X", text="") 654 | 655 | 656 | class DOFU_PT_camera(DOFU_panel, bpy.types.Panel): 657 | bl_label = "Camera Settings" 658 | bl_parent_id = "DOFU_PT_main_panel" 659 | 660 | def draw(self, context): 661 | dofu = context.scene.dof_utils #cam_ob = scene.camera.data 662 | cam_ob = context.active_object.data 663 | active_flag = not dofu.use_cursor and cam_ob.dof.focus_object is None 664 | 665 | layout = self.layout 666 | layout.use_property_split = True 667 | layout.active = cam_ob.dof.use_dof 668 | 669 | col = layout.column() 670 | col.prop(cam_ob.dof, "aperture_fstop", text="F-Stop") 671 | dis = col.column() 672 | dis.enabled = active_flag # active 673 | dis.prop(cam_ob.dof, "focus_distance", text="Focus Distance") 674 | col.prop(cam_ob, "lens") 675 | col.prop(cam_ob.dof, "focus_object", text="Focus Object") 676 | layout.separator() 677 | 678 | 679 | class DOFU_PT_visualize(DOFU_panel, bpy.types.Panel): 680 | bl_label = "Visualization" 681 | bl_parent_id = "DOFU_PT_main_panel" 682 | 683 | def draw(self, context): 684 | dofu = context.scene.dof_utils #cam_ob = scene.camera.data 685 | 686 | layout = self.layout 687 | layout.use_property_split = True 688 | layout.active = context.active_object.data.dof.use_dof 689 | 690 | col = layout.column() 691 | col.prop(dofu, "color_limits", text="Color") 692 | col.prop(dofu, "size_limits") 693 | col.prop(dofu, "opacity_limits") 694 | col.prop(dofu, "segments_limits") 695 | col = layout.column() 696 | col.prop(dofu, "overlay", text="Overlay Limits")#, toggle=True, icon="GHOST_ENABLED") 697 | col.prop(dofu, "draw_focus") #, toggle=True, icon="FORCE_FORCE") 698 | layout.separator() 699 | 700 | 701 | # ------------------------------------------------------------------------ 702 | # Registration 703 | # ------------------------------------------------------------------------ 704 | 705 | classes = ( 706 | DOFU_AP_preferences, 707 | DOFU_PG_settings, 708 | DOFU_OT_focusPicking, 709 | DOFU_OT_visualizeLimits, 710 | DOFU_OT_killVisualization, 711 | DOFU_OT_killFocusPicking, 712 | DOFU_OT_viewportReset, 713 | DOFU_OT_preferencesReset, 714 | DOFU_PT_main_panel, 715 | DOFU_PT_camera, 716 | DOFU_PT_visualize 717 | ) 718 | 719 | 720 | def register(): 721 | from bpy.utils import register_class 722 | for cls in classes: 723 | register_class(cls) 724 | 725 | bpy.types.Scene.dof_utils = bpy.props.PointerProperty(type=DOFU_PG_settings) 726 | 727 | def unregister(): 728 | bpy.ops.dof_utils.reset_preferences() 729 | 730 | from bpy.utils import unregister_class 731 | for cls in reversed(classes): 732 | unregister_class(cls) 733 | 734 | del bpy.types.Scene.dof_utils 735 | 736 | if __name__ == "__main__": 737 | register() 738 | --------------------------------------------------------------------------------