├── LICENSE ├── README.md ├── fuzzy_tools.py └── images ├── CameraControl_Scene_v3_operators.png ├── CameraControl_SelectedCamera_v3_operators.png ├── Collection_Cameras.png ├── Collection_Set_Floor.png ├── Collection_Set_RimLight.png ├── Collection_Set_Sun.png ├── FloorHoldout_disabled.png ├── FloorHoldout_enabled.png ├── FuzzyView_Lens.png ├── FuzzyViewport_Hair.png ├── FuzzyViewport_Simplify.png ├── FuzzyViewport_ViewportDisplay.png ├── ICON_EARTH.png ├── MotionBlur_SceneAnimation.png ├── MoveKeyframesAndMarkers.png ├── SceneBuilder_Background_v2.png ├── SceneBuilder_BuildAll.png ├── SceneBuilder_BuildInParts.png ├── SceneBuilder_FloorShadow.png ├── SceneBuilder_FloorShadow_v2.png ├── SceneBuilder_FuzzyFloor.png ├── SceneBuilder_HDRI.png ├── SceneBuilder_HDRI_v2.png ├── cameraControl_animate.png ├── floorNormal_3DView.png ├── floorNormal_Modifier.png ├── floorNormal_back.png ├── floorNormal_front.png ├── floor_clampDodge.png ├── fuzzytools_banner.png ├── renameVariant_after.png └── renameVariant_before.png /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Fuzzy Tools banner](https://github.com/sagotoons/fuzzytools/blob/5aabad83324189747782a9c5326fe3565c223e19/images/fuzzytools_banner.png) 2 | The Fuzzy Tools add-on is a collection of tools, heavily focused on efficiency and a quick workflow with a multi-camera scene in mind. Some of Fuzzy Tools was demonstrated during my BCON presentations: 3 | - BCON23 - 'Journey Towards an Efficient 1-Person Pipeline': www.youtube.com/watch?v=W01lQrcCz9s 4 | - BCON24 - 'Fast Content Creation with Code and Creativity': www.youtube.com/watch?v=sb91eRKxN4s 5 | 6 | _Fuzzy Tools is specifically created for a fast EEVEE pipeline, but many aspects still work in Cycles._ 7 | 8 | # Features 9 | Some unique features that come with Fuzzy Tools: 10 | - Set up lights, camera, world and floor (shadow only) with just one or a few buttons 11 | - Use markers to enable/disable Motion Blur and change shutter time 12 | - Alt+M to [Move Keyframes and Markers](https://github.com/sagotoons/fuzzytools/wiki/Operator-‐-Move-Keyframes-and-Markers) from the current frame, regardless of selection 13 | 14 | ## Panels 15 | 16 | Fuzzy Tools creates the following panels in the sidebar (N): 17 | 18 | ### [1. Scene Builder](https://github.com/sagotoons/fuzzytools/wiki/Scene-Builder) 19 | Quickly set up your scene, including: 20 | - Optimized camera(s) 21 | - Floor with shadow only 22 | - World shader with stylized background 23 | - Sunlight and rim light 24 | - Optimize EEVEE settings like AO, Shadows and Bloom in Legacy, or Ray Tracing in Next 25 | 26 | ### [2. Fuzzy View](https://github.com/sagotoons/fuzzytools/wiki/Fuzzy-View) 27 | Quick access to different viewport and hair settings, including: 28 | - Viewport Lens 29 | - Use Simplify 30 | - Hide/Unhide all Hair 31 | - Hair Type 32 | 33 | ### [3. Camera Control](https://github.com/sagotoons/fuzzytools/wiki/Camera-Control) 34 | Quick access to different camera settings and operators, including: 35 | - Selecting and changing Active Camera 36 | - Set Markers to animate Motion Blur and Shutter 37 | - Bind Camera to Markers 38 | 39 | # 40 | _WARNING: Fuzzy Tools 3.0 will break elements created with older versions of Fuzzy Tools. Specifically, 'Fuzzy floor' and 'Fuzzy World'. These elements would have to be recreated for optimal results._ 41 | 42 | _Fuzzy Tools is supported for blender 3.6 up to 4.2._ 43 | # 44 | If you enjoy Fuzzy Tools, please consider supporting me on PayPal: 45 | 46 | https://www.paypal.me/sagotoons 47 | -------------------------------------------------------------------------------- /fuzzy_tools.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | bl_info = { 4 | "name" : "Fuzzy Tools", 5 | "author" : "Sacha Goedegebure", 6 | "version" : (3,0,2), 7 | "blender" : (3,6,0), 8 | "location" : "View3d > Sidebar > Fuzzy. Alt+M to move keyframes and markers", 9 | "description" : "Tools for an efficient 1-person pipeline and multi-camera workflow", 10 | "doc_url" : "https://github.com/sagotoons/fuzzytools/wiki", 11 | "tracker_url" : "https://github.com/sagotoons/fuzzytools/issues", 12 | "category" : "Interface", 13 | } 14 | 15 | 16 | import bpy 17 | 18 | import math 19 | 20 | from bpy.props import (BoolProperty, 21 | FloatProperty, 22 | IntProperty, 23 | FloatVectorProperty, 24 | EnumProperty, 25 | PointerProperty, 26 | ) 27 | from bpy.types import (Panel, 28 | Operator, 29 | PropertyGroup, 30 | ) 31 | from math import radians, degrees 32 | 33 | from bpy.app.handlers import persistent 34 | 35 | 36 | # check blender version for Eevee Next 37 | def is_next_version(min_version=(4, 2, 0)): 38 | current_version = bpy.app.version 39 | return current_version >= min_version 40 | 41 | 42 | # find HDRI studio light when used in Fuzzy World shader during start up 43 | @persistent 44 | def reload_image(_): 45 | world = bpy.context.scene.world 46 | nodes = world.node_tree.nodes 47 | if world.name != 'Fuzzy World': 48 | return 49 | try: 50 | node = nodes['World HDRI'] 51 | except KeyError: ## for files created with Fuzzy Tools 2.0.0 or older 52 | nodes['Environment Texture'].name = 'World HDRI' 53 | node = nodes['World HDRI'] 54 | # remove suffix 55 | name = node.image.name.rsplit('.', 1)[0] 56 | valid_names = {'city', 'courtyard', 'forest', 'interior', 'night', 'studio', 'sunrise', 'sunset'} 57 | if name not in valid_names or node.image.file_format == 'OPEN_EXR': 58 | return 59 | node.image.name = name + "_old" 60 | try: 61 | hdri = bpy.data.images.load( 62 | bpy.context.preferences.studio_lights[name + '.exr'].path, check_existing=True 63 | ) 64 | node.image = hdri 65 | except Exception as e: 66 | print(f"Error loading HDRI: {e}") 67 | 68 | 69 | # handler initialized during render 70 | @persistent 71 | def auto_animate_scene(scene, context): 72 | prop = bpy.context.scene.fuzzy_props 73 | prop.scene_animate = True 74 | bpy.app.handlers.render_init.append(auto_animate_scene) 75 | 76 | # handler for removing previous handler during canceling or completing render 77 | @persistent 78 | def disable_animate_scene(scene, context): 79 | prop = bpy.context.scene.fuzzy_props 80 | 81 | handlers = bpy.app.handlers.frame_change_post 82 | check_handlers = [handler for handler in handlers if 'check' in str(handler)] 83 | # method below due to unique functions for handlers 84 | if len(check_handlers) == 1: 85 | prop.scene_animate = False 86 | if len(check_handlers) == 2: 87 | prop.scene_animate = False 88 | prop.scene_animate = False 89 | prop.scene_animate = True 90 | bpy.app.handlers.render_cancel.append(disable_animate_scene) 91 | bpy.app.handlers.render_complete.append(disable_animate_scene) 92 | 93 | 94 | # fix naming after upgrades in v3.0.2 95 | @persistent 96 | def name_fix(_): 97 | objs = bpy.data.objects 98 | if 'Fuzzy floor' in objs: 99 | floor = objs['Fuzzy floor'] 100 | floor.name = 'FuzzyFloor' 101 | mods = floor.modifiers 102 | if 'Normal Direction' in mods: 103 | mods['Normal Direction'].name = 'NormalDirection' 104 | if 'floor normal' in objs: 105 | objs['floor normal'].name = 'FloorNormal' 106 | 107 | 108 | # ------------------------------------------------------------------------ 109 | # SCENE PROPERTIES 110 | # ------------------------------------------------------------------------ 111 | 112 | def check(self): 113 | # check motion blur markers 114 | scene = bpy.context.scene 115 | frame = scene.frame_current 116 | markers = scene.timeline_markers 117 | 118 | if is_next_version(): 119 | version = scene.render 120 | else: 121 | version = scene.eevee 122 | 123 | if any(marker.name.startswith('mblur') for marker in markers): 124 | mblur = False 125 | # check mblur markers in reversed order 126 | for k, v in reversed(sorted(markers.items(), key=lambda it: it[1].frame)): 127 | if v.frame <= frame and v.name.startswith('mblur_on'): 128 | version.use_motion_blur = True 129 | val = v.name.strip('mblur_on') 130 | try: 131 | version.motion_blur_shutter = float(val) 132 | except ValueError: 133 | pass 134 | mblur = True 135 | break 136 | elif v.frame <= frame and v.name == 'mblur_off': 137 | version.use_motion_blur = False 138 | mblur = True 139 | break 140 | # check for first mblur marker 141 | if not mblur: 142 | for k, v in sorted(markers.items(), key=lambda it: it[1].frame): 143 | if v.frame >= frame and v.name.startswith('mblur_on'): 144 | version.use_motion_blur = True 145 | val = v.name.strip('mblur_on') 146 | try: 147 | version.motion_blur_shutter = float(val) 148 | except ValueError: 149 | pass 150 | break 151 | elif v.frame >= frame and v.name == 'mblur_off': 152 | version.use_motion_blur = False 153 | break 154 | 155 | 156 | def check_scene(self, context): 157 | if self.scene_animate: 158 | bpy.app.handlers.frame_change_post.append(check) 159 | else: 160 | bpy.app.handlers.frame_change_post.remove(check) 161 | 162 | 163 | class FuzzyProperties(PropertyGroup): 164 | 165 | scene_animate: BoolProperty( 166 | name='Update Motion Blur', 167 | description="""Update animated motion blur properties in viewport. 168 | Enables automatically during rendering""", 169 | default=False, 170 | update=check_scene 171 | ) 172 | 173 | fuzzy_color1: FloatVectorProperty( 174 | name="Palette Color 1", 175 | subtype='COLOR', 176 | default=(0.09, 0.17, 1.0), 177 | min=0.0, max=1.0, 178 | ) 179 | 180 | fuzzy_color2: FloatVectorProperty( 181 | name="Palette Color 2", 182 | subtype='COLOR', 183 | default=(0.02, 0.05, 0.40), 184 | min=0.0, max=1.0, 185 | ) 186 | 187 | 188 | # ------------------------------------------------------------------------ 189 | # OPERATOR - Build All 190 | # ------------------------------------------------------------------------ 191 | 192 | class SCENE_OT_build_all(Operator): 193 | """Place a camera, floor, sun light and rim light. Create a new Fuzzy World. Optimize Eevee settings. 194 | Replace existing floor and active world, if available. 195 | Delete the default cube, camera, and light""" 196 | bl_idname = "scene.build_all" 197 | bl_label = "Build All" 198 | bl_options = {'UNDO'} 199 | 200 | @classmethod 201 | def poll(cls, context): 202 | return context.mode == 'OBJECT' 203 | 204 | def execute(self, context): 205 | ops = bpy.ops 206 | ops.object.fuzzy_camera() 207 | ops.mesh.fuzzy_floor() 208 | ops.world.fuzzy_sky() 209 | ops.object.fuzzy_sun() 210 | ops.object.fuzzy_rimlight() 211 | ops.scene.fuzzy_eevee() 212 | 213 | self.report({'INFO'}, "POP!") 214 | return {'FINISHED'} 215 | 216 | 217 | # ------------------------------------------------------------------------ 218 | # OPERATOR - Camera 219 | # ------------------------------------------------------------------------ 220 | 221 | class OBJECT_OT_fuzzy_camera(Operator): 222 | """Place an optimized camera. 223 | Delete the default camera""" 224 | bl_idname = "object.fuzzy_camera" 225 | bl_label = "Build Camera" 226 | bl_options = {'UNDO'} 227 | 228 | @classmethod 229 | def poll(cls, context): 230 | return context.mode == 'OBJECT' 231 | 232 | def execute(self, context): 233 | scene = context.scene 234 | objects = scene.objects 235 | objs = bpy.data.objects 236 | 237 | # CAMERA PROPERTIES 238 | loc_y = -25 239 | loc_z = 2.5 240 | rot_x = 90 241 | lens = 85 242 | clip_start = 1 243 | clip_end = 250 244 | 245 | # Delete default camera 246 | if 'Camera' in objects: 247 | objs.remove(objs["Camera"]) 248 | 249 | # Add new camera and set rotation 250 | bpy.ops.object.camera_add(rotation=(radians(rot_x), 0, 0)) 251 | ob = context.active_object 252 | 253 | # List all objects with "CAM." prefix 254 | cams = [obj for obj in objs if obj.name.startswith("CAM.")] 255 | 256 | if not cams: 257 | ob.name = "CAM.001" 258 | ob.location = (0, loc_y, loc_z) 259 | else: 260 | # Remove "CAM." from names to leave only numbers + letter 261 | cams_ABC = [s[4:] for s in [obj.name for obj in cams]] 262 | # Remove possible letter suffix 263 | cams_no_ABC = [s[:3] for s in cams_ABC] 264 | # Find the smallest available number 265 | available_numbers = set(range(1, max(map(int, cams_no_ABC)) + 2)) 266 | used_numbers = set(map(int, cams_no_ABC)) 267 | i = min(available_numbers - used_numbers) 268 | # Change name of camera with increasing number 269 | ob.name = f"CAM.{i:03}" 270 | # Place camera distance away from previous camera's origin 271 | ob.location = (1.5*(-1 + i), loc_y, loc_z) 272 | 273 | # create collection 'Cameras' if it doesn't exist yet 274 | link_to_name = 'Cameras' 275 | link_to = scene.collection.children.get(link_to_name) 276 | if link_to is None: 277 | link_to = bpy.data.collections.new(link_to_name) 278 | scene.collection.children.link(link_to) 279 | 280 | # link new camera to collection 'Cameras' 281 | oldcoll = ob.users_collection[0] 282 | if oldcoll.name == 'Scene Collection': 283 | context.collection.objects.unlink(ob) 284 | else: 285 | oldcoll.objects.unlink(ob) 286 | bpy.data.collections['Cameras'].objects.link(ob) 287 | 288 | ob.show_name = True 289 | data = ob.data 290 | data.name = ob.name 291 | 292 | # optimize camera settings 293 | data.show_limits = False 294 | data.show_name = True 295 | data.clip_start = clip_start 296 | data.clip_end = clip_end 297 | data.lens = lens 298 | data.passepartout_alpha = 0.8 299 | data.dof.focus_distance = abs(loc_y) 300 | 301 | # make new camera active 302 | objects = context.view_layer.objects 303 | try: 304 | if ob.name in objects: 305 | objects.active = ob 306 | except RuntimeError: 307 | pass 308 | 309 | self.report({'INFO'}, f"Camera '{ob.name}' added to scene") 310 | return {'FINISHED'} 311 | 312 | 313 | # ------------------------------------------------------------------------ 314 | # OPERATOR - Fuzzy Floor (shadow only) 315 | # ------------------------------------------------------------------------ 316 | 317 | class MESH_OT_fuzzy_floor(Operator): 318 | """Place a floor with shadow only and replace the old one. 319 | Delete the default cube""" 320 | bl_idname = "mesh.fuzzy_floor" 321 | bl_label = "Build Floor" 322 | bl_options = {'UNDO'} 323 | 324 | @classmethod 325 | def poll(cls, context): 326 | return context.mode == 'OBJECT' 327 | 328 | def execute(self, context): 329 | scene = context.scene 330 | objects = scene.objects 331 | 332 | # delete objects 333 | for name in ["Cube", "FuzzyFloor", "FloorNormal"]: 334 | if name in objects: 335 | obj = bpy.data.objects[name] 336 | if name == "Cube": 337 | if hasattr(obj.data, 'polygons') and len(obj.data.polygons) == 6: 338 | bpy.data.objects.remove(obj) 339 | else: 340 | bpy.data.objects.remove(obj) 341 | 342 | # add floor 343 | bpy.ops.mesh.primitive_plane_add(size=60, location=(0, 0, 0)) 344 | 345 | # name new Plane 'FuzzyFloor' 346 | floor = context.active_object 347 | floor.name = "FuzzyFloor" 348 | 349 | # create collection 'Set' if it doesn't exist yet 350 | link_to_name = 'Set' 351 | try: 352 | link_to = scene.collection.children[link_to_name] 353 | except KeyError: 354 | link_to = bpy.data.collections.new(link_to_name) 355 | scene.collection.children.link(link_to) 356 | 357 | # link floor to collection 'Set' 358 | oldcoll = floor.users_collection[0] 359 | if oldcoll.name == 'Scene Collection': 360 | context.collection.objects.unlink(floor) 361 | else: 362 | oldcoll.objects.unlink(floor) 363 | bpy.data.collections['Set'].objects.link(floor) 364 | 365 | # create empty as Target for FloorNormal Edit modifier 366 | bpy.ops.object.empty_add(location=(15, -20, 20)) 367 | empty = context.object 368 | empty.name = "FloorNormal" 369 | empty.empty_display_size = 6 370 | empty.empty_display_type = 'SINGLE_ARROW' 371 | link_to_name = 'Set' 372 | track = empty.constraints.new('DAMPED_TRACK') 373 | track.target = floor 374 | track.track_axis = 'TRACK_Z' 375 | 376 | # link empty to collection 'Set' 377 | oldcoll = empty.users_collection[0].name 378 | if oldcoll == 'Scene Collection': 379 | context.collection.objects.unlink(empty) 380 | else: 381 | bpy.data.collections[oldcoll].objects.unlink(empty) 382 | bpy.data.collections['Set'].objects.link(empty) 383 | 384 | # objects settings ## error exception added for blender 4.1 385 | try: 386 | floor.data.use_auto_smooth = True 387 | except AttributeError: 388 | pass 389 | 390 | # create modifier 'Normal Edit' and set empty as Target 391 | normal = floor.modifiers.new("NormalDirection", 'NORMAL_EDIT') 392 | normal.mode = 'DIRECTIONAL' 393 | normal.use_direction_parallel = True 394 | normal.target = bpy.data.objects["FloorNormal"] 395 | normal.no_polynors_fix = True 396 | 397 | # object settings 398 | floor.hide_select = True 399 | floor.show_wire = True 400 | 401 | # Get material 402 | oldmat = bpy.data.materials.get("floor_shadow") 403 | if oldmat is not None: 404 | oldmat.name = "floor_shadow_old" 405 | 406 | # create material 407 | mat = bpy.data.materials.new(name="floor_shadow") 408 | # Assign it to object 409 | if floor.data.materials: 410 | # assign to 1st material slot 411 | floor.data.materials[0] = mat 412 | else: 413 | # no slots 414 | floor.data.materials.append(mat) 415 | 416 | mat.use_nodes = True 417 | 418 | # build node shader 419 | nodes = bpy.data.materials['floor_shadow'].node_tree.nodes 420 | nodes.remove(nodes.get('Principled BSDF')) 421 | 422 | matoutput = nodes.get("Material Output") 423 | matoutput.location = (600, 80) 424 | matoutput.target = 'EEVEE' 425 | 426 | mixshader = nodes.new("ShaderNodeMixShader") 427 | mixshader.location = (200, 60) 428 | 429 | shadow = nodes.new("ShaderNodeBsdfDiffuse") 430 | shadow.location = (0, 10) 431 | shadow.inputs[0].default_value = (0, 0, 0, 1) 432 | 433 | holdout = nodes.new("ShaderNodeHoldout") 434 | holdout.location = (-200, -160) 435 | 436 | clamp_shadow = nodes.new("ShaderNodeClamp") 437 | clamp_shadow.location = (-200, 240) 438 | 439 | mix_AO = nodes.new("ShaderNodeMixRGB") 440 | mix_AO.location = (-570, 100) 441 | mix_AO.inputs[0].default_value = 0.7 442 | mix_AO.blend_type = 'MULTIPLY' 443 | mix_AO.mute = True 444 | 445 | shader_RGB = nodes.new("ShaderNodeShaderToRGB") 446 | shader_RGB.location = (-770, 0) 447 | 448 | diffuse = nodes.new("ShaderNodeBsdfDiffuse") 449 | diffuse.location = (-970, -100) 450 | diffuse.inputs[0].default_value = (1, 1, 1, 1) 451 | 452 | dodge_floor = nodes.new("ShaderNodeMixRGB") 453 | dodge_floor.location = (-380, 60) 454 | dodge_floor.inputs[0].default_value = 1 455 | dodge_floor.blend_type = 'DODGE' 456 | 457 | power = nodes.new("ShaderNodeMath") 458 | power.location = (0, 180) 459 | power.operation = 'POWER' 460 | power.use_clamp = True 461 | 462 | value = nodes.new("ShaderNodeMath") 463 | value.name = "Shadow Value" 464 | value.location = (-200, 60) 465 | value.operation = 'MULTIPLY_ADD' 466 | value.inputs[0].default_value = 0 467 | value.inputs[1].default_value = -1 468 | value.inputs[2].default_value = 1 469 | 470 | value_dodge = nodes.new("ShaderNodeMix") 471 | value_dodge.name = "Dodge Value" 472 | value_dodge.location = (-570, -150) 473 | value_dodge.inputs[0].default_value = 0.1 474 | value_dodge.inputs[3].default_value = 1 475 | 476 | value_clamp = nodes.new("ShaderNodeMix") 477 | value_clamp.name = "Clamp Value" 478 | value_clamp.location = (-380, 260) 479 | value_clamp.inputs[0].default_value = 0.1 480 | value_clamp.inputs[3].default_value = 1 481 | 482 | alpha_mix = nodes.new("ShaderNodeMixShader") 483 | alpha_mix.name = "Floor Alpha" 484 | alpha_mix.location = (0, -160) 485 | 486 | BG_group = nodes.new("ShaderNodeGroup") 487 | BG_group.name = "Floor Group" 488 | BG_group.location = (-200, -260) 489 | 490 | # check for Fuzzy BG node group 491 | BG = 'Fuzzy BG' 492 | groups = bpy.data.node_groups 493 | if BG not in groups: 494 | alpha_mix.inputs[0].default_value = 0.0 495 | else: 496 | alpha_mix.inputs[0].default_value = 1.0 497 | BG_group.node_tree = groups[BG] 498 | 499 | # link nodes 500 | link = mat.node_tree.links.new 501 | link(mixshader.outputs[0], matoutput.inputs[0]) 502 | link(shadow.outputs[0], mixshader.inputs[1]) 503 | link(holdout.outputs[0], alpha_mix.inputs[1]) 504 | link(alpha_mix.outputs[0], mixshader.inputs[2]) 505 | link(clamp_shadow.outputs[0], power.inputs[0]) 506 | link(mix_AO.outputs[0], dodge_floor.inputs[1]) 507 | link(value.outputs[0], power.inputs[1]) 508 | link(power.outputs[0], mixshader.inputs[0]) 509 | link(shader_RGB.outputs[0], mix_AO.inputs[1]) 510 | link(diffuse.outputs[0], shader_RGB.inputs[0]) 511 | link(dodge_floor.outputs[0], clamp_shadow.inputs[0]) 512 | link(value_dodge.outputs[0], dodge_floor.inputs[2]) 513 | link(value_clamp.outputs[0], clamp_shadow.inputs[1]) 514 | if BG in groups: 515 | link(BG_group.outputs[0], alpha_mix.inputs[2]) 516 | 517 | # material settings 518 | mat.use_backface_culling = True 519 | mat.blend_method = 'BLEND' 520 | 521 | # cycles material nodes 522 | matoutput2 = nodes.new("ShaderNodeOutputMaterial") 523 | matoutput2.location = (400, -80) 524 | matoutput2.target = 'CYCLES' 525 | link(diffuse.outputs[0], matoutput2.inputs[0]) 526 | # cycles material settings 527 | floor.is_shadow_catcher = True 528 | floor.visible_diffuse = False 529 | floor.visible_glossy = False 530 | floor.visible_transmission = False 531 | 532 | # viewport & outliner settings 533 | screens = bpy.data.screens 534 | for scr in screens: 535 | for area in scr.areas: 536 | if area.type == 'VIEW_3D': 537 | area.spaces[0].overlay.show_relationship_lines = False 538 | area.spaces[0].clip_start = 0.1 539 | # elif area.type == 'OUTLINER': 540 | # area.spaces[0].show_restrict_column_viewport = True 541 | # area.spaces[0].show_restrict_column_select = True 542 | 543 | # 4.2 or above 544 | if is_next_version(): 545 | mix_AO.mute = False 546 | mix_AO.name = "AO Factor" 547 | 548 | AO = nodes.new("ShaderNodeAmbientOcclusion") 549 | AO.name = "AO" 550 | AO.location = (-770, 230) 551 | AO.inputs[1].default_value = 1.6 552 | link(AO.outputs[1], mix_AO.inputs[2]) 553 | 554 | mixshader2 = nodes.new("ShaderNodeMixShader") 555 | mixshader2.location = (400, 60) 556 | link(mixshader.outputs[0], mixshader2.inputs[1]) 557 | link(mixshader2.outputs[0], matoutput.inputs[0]) 558 | 559 | lightpath = nodes.new("ShaderNodeLightPath") 560 | lightpath.location = (200, 140) 561 | for output in lightpath.outputs: 562 | output.hide = True 563 | link(lightpath.outputs[1], mixshader2.inputs[0]) 564 | 565 | transp = nodes.new("ShaderNodeBsdfTransparent") 566 | transp.location = (200, -80) 567 | link(transp.outputs[0], mixshader2.inputs[2]) 568 | else: 569 | mat.shadow_method = 'NONE' 570 | 571 | self.report({'INFO'}, f"'{floor.name}' and '{empty.name}' added to scene") 572 | return {'FINISHED'} 573 | 574 | 575 | # ------------------------------------------------------------------------ 576 | # OPERATOR - World (Sky) 577 | # ------------------------------------------------------------------------ 578 | 579 | class WORLD_OT_fuzzy_sky(Operator): 580 | """Create a new world and replace the active one""" 581 | bl_idname = "world.fuzzy_sky" 582 | bl_label = "New Fuzzy Sky" 583 | bl_options = {'UNDO'} 584 | 585 | @classmethod 586 | def poll(cls, context): 587 | return context.mode == 'OBJECT' 588 | 589 | def execute(self, context): 590 | scene = context.scene 591 | # rename "Fuzzy World" if it exists 592 | if "Fuzzy World" in bpy.data.worlds: 593 | bpy.data.worlds['Fuzzy World'].name = 'World_old' 594 | else: 595 | pass 596 | 597 | # create "Fuzzy World", make it scene world & enable Use Nodes 598 | bpy.data.worlds.new("Fuzzy World") 599 | scene.world = bpy.data.worlds['Fuzzy World'] 600 | scene.world.use_nodes = True 601 | 602 | # build node shader 603 | nodes = scene.world.node_tree.nodes 604 | 605 | nodes.remove(nodes.get('Background')) 606 | 607 | # HDR nodes 608 | worldoutput = nodes.get("World Output") 609 | worldoutput.location = (900, 50) 610 | 611 | # dictionary 612 | ref = {} 613 | # list with ref_name, name, type, locx, locy 614 | node_list = [ 615 | ('texcoord1', "Texture Coordinate", "TexCoord", -1000, 440), # row 1 616 | ('mapskytex1',"HDRI Delta Rot", "Mapping", -800, 440), # row 2 617 | ('mapskytex2',"HDRI Rotation", "Mapping", -600, 440), # row 3 618 | ('clamprefl', "Clamp Reflection", "Value", -600, 60), 619 | ('multiply', "Multiply", "Math", -600, -40), 620 | ('skytex', "World HDRI", "TexEnvironment", -400, 400), # row 4 621 | ('greater', "Greater Than", "Math", -400, 160), 622 | ('lightpath', "Light Path", "LightPath", -400, -40), 623 | ('sepHSV', "Separate Color", "SeparateColor", -100, 400), # row 5 624 | ('darken', "Darken", "MixRGB", -100, 240), 625 | ('mixrefl', "Mix Reflection", "MixRGB", 100, 400), #row6 626 | ('comHSV', "Combine Color", "CombineColor", 300, 400), #row7 627 | ('BG1', "HDRI Strength", "Background", 500, 160), #row8 628 | ('BG2', "Background", "Background", 500, -100), 629 | ('mixshader',"Mix Shader", "MixShader", 700, 60), #row9 630 | ] 631 | 632 | # create nodes 633 | for ref_name, name, type, locx, locy in node_list: 634 | node = nodes.new("ShaderNode"+type) 635 | node.location = (locx, locy) 636 | node.label = name 637 | node.name = name 638 | 639 | # Save ref_name in dictionary 640 | ref[ref_name] = node 641 | 642 | # extra node properties 643 | ref['mapskytex1'].inputs[2].default_value[2] = radians(90) 644 | ref['clamprefl'].outputs[0].default_value = 2 645 | ref['multiply'].operation = 'MULTIPLY' 646 | ref['multiply'].inputs[1].default_value = 10 647 | ref['greater'].operation = 'GREATER_THAN' 648 | ref['greater'].inputs[1].default_value = 0 649 | for output in ref['lightpath'].outputs: 650 | output.hide = True 651 | ref['sepHSV'].mode = 'HSV' 652 | ref['darken'].blend_type = 'DARKEN' 653 | ref['comHSV'].mode = 'HSV' 654 | 655 | # connect nodes 656 | link = scene.world.node_tree.links.new 657 | link(ref['texcoord1'].outputs[0], ref['mapskytex1'].inputs[0]) 658 | link(ref['mapskytex1'].outputs[0], ref['mapskytex2'].inputs[0]) 659 | link(ref['mapskytex2'].outputs[0], ref['skytex'].inputs[0]) 660 | link(ref['clamprefl'].outputs[0], ref['multiply'].inputs[0]) 661 | link(ref['clamprefl'].outputs[0], ref['greater'].inputs[0]) 662 | link(ref['multiply'].outputs[0], ref['darken'].inputs[2]) 663 | link(ref['skytex'].outputs[0], ref['sepHSV'].inputs[0]) 664 | link(ref['greater'].outputs[0], ref['mixrefl'].inputs[0]) 665 | link(ref['lightpath'].outputs[3], ref['darken'].inputs[0]) 666 | link(ref['lightpath'].outputs[0], ref['mixshader'].inputs[0]) 667 | for i in range(2): 668 | link(ref['sepHSV'].outputs[i], ref['comHSV'].inputs[i]) 669 | link(ref['sepHSV'].outputs[2], ref['darken'].inputs[1]) 670 | link(ref['sepHSV'].outputs[2], ref['mixrefl'].inputs[1]) 671 | link(ref['darken'].outputs[0], ref['mixrefl'].inputs[2]) 672 | link(ref['mixrefl'].outputs[0], ref['comHSV'].inputs[2]) 673 | link(ref['comHSV'].outputs[0], ref['BG1'].inputs[0]) 674 | link(ref['BG1'].outputs[0], ref['mixshader'].inputs[1]) 675 | link(ref['BG2'].outputs[0], ref['mixshader'].inputs[2]) 676 | link(ref['mixshader'].outputs[0], worldoutput.inputs[0]) 677 | 678 | # load the texture from Blender data folder 679 | hdri = bpy.data.images.load( 680 | context.preferences.studio_lights['sunset.exr'].path, check_existing=True) 681 | ref['skytex'].image = hdri 682 | 683 | # check for Fuzzy BG node group and remove 684 | BG = 'Fuzzy BG' 685 | groups = bpy.data.node_groups 686 | if BG in groups: 687 | groups.remove(groups[BG]) 688 | 689 | # create Fuzzy BG node group 690 | BG_group = groups.new(BG, 'ShaderNodeTree') 691 | if bpy.app.version_string.startswith('3'): 692 | BG_group.outputs.new('NodeSocketColor', "Color") 693 | else: 694 | BG_group.interface.new_socket("Color", in_out='OUTPUT', 695 | socket_type='NodeSocketColor') 696 | 697 | # create empty group node and apply Fuzzy BG 698 | group = nodes.new("ShaderNodeGroup") 699 | group.location = (300, -100) 700 | group.name = "BG Group" 701 | group.node_tree = BG_group 702 | 703 | # BG group nodes 704 | nodes = BG_group.nodes 705 | 706 | # dictionary 707 | ref = {} 708 | # list with ref_name, name, type, locx, locy 709 | node_list = [ 710 | ('texcoord2', "Tex Coord", "TexCoord", -1860, -100), # row 1 711 | ('gradscale', "Scale Gradient", "Mix", -1860, -500), 712 | ('radialloc', "Radial Location", "VectorMath", -1650, 220), # row 2 713 | ('radialscale', "Radial Scale", "VectorMath", -1650, -40), 714 | ('vectrans', "", "VectorTransform", -1650, -300), 715 | ('power', "", "Math", -1650, -480), 716 | ('mapsphere', "", "Mapping", -1450, 50), # row 3 717 | ('divide', "", "MixRGB", -1450, -350), 718 | ('maplinear3d', "", "Mapping", -1250, -400), # row 4 719 | ('maplinear', "", "Mapping", -1250, -30), 720 | ('gradsphere', "", "TexGradient", -1050, -80), #row5 721 | ('window3d', "Window to 3D", "MixRGB", -1050, -240), 722 | ('invert', "", "Invert", -880, -80), #row6 723 | ('gradlinear', "Gradient Linear", "TexGradient", -880, -240), 724 | ('rampradial', "Radial Ease", "ValToRGB", -700, 20), #row7 725 | ('ramplinear', "Linear Ease", "ValToRGB", -700, -200), 726 | ('col1', "BG Color 1", "RGB", -680, -440), 727 | ('col2', "BG Color 2", "RGB", -680, -640), 728 | ('linear2ease', "Linear Ease", "Mix", -420, -200), #row8 729 | ('swapcol1', "Swap Colors 1", "MixRGB", -420, -440), 730 | ('swapcol2', "Swap Colors 2", "MixRGB", -420, -640), 731 | ('radial2linear', "Radial to Linear", "MixRGB", -220, -80), #row9 732 | ('colgradient', "Color Gradient", "MixRGB",-40, -300), #row10 733 | ('flat2gradient', "Flat to Gradient", "MixRGB", 140, -100), #row11 734 | ] 735 | 736 | # create nodes 737 | for ref_name, name, type, locx, locy in node_list: 738 | node = nodes.new("ShaderNode"+type) 739 | node.location = (locx, locy) 740 | node.label = name 741 | node.name = name 742 | 743 | # Save ref_name in dictionary 744 | ref[ref_name] = node 745 | 746 | # extra node properties 747 | ref['radialloc'].inputs[1].default_value = (0.5, 0.5, 0) 748 | ref['radialscale'].operation = 'MULTIPLY' 749 | ref['radialscale'].inputs[0].default_value = (1, 1, 0) 750 | ref['radialscale'].inputs[1].default_value = (0.71, 0.71, 1) 751 | ref['divide'].blend_type = 'DIVIDE' 752 | ref['divide'].inputs[0].default_value = 1 753 | ref['gradscale'].inputs[2].default_value = 0.001 754 | ref['gradscale'].inputs[3].default_value = 1 755 | ref['gradsphere'].gradient_type = 'SPHERICAL' 756 | ref['maplinear'].inputs[2].default_value[2] = 1.5708 757 | ref['maplinear'].vector_type = 'TEXTURE' 758 | ref['maplinear3d'].inputs[1].default_value[2] = -0.5 759 | ref['maplinear3d'].inputs[2].default_value[1] = -1.5708 760 | ref['maplinear3d'].vector_type = 'TEXTURE' 761 | ref['mapsphere'].vector_type = 'TEXTURE' 762 | ref['power'].inputs[1].default_value = 2 763 | ref['power'].operation = 'POWER' 764 | ref['ramplinear'].color_ramp.interpolation = "EASE" 765 | ref['rampradial'].color_ramp.interpolation = "EASE" 766 | ref['col1'].outputs[0].default_value = (0.09, 0.17, 1, 1) 767 | ref['col2'].outputs[0].default_value = (0.02, 0.05, 0.40, 1) 768 | ref['vectrans'].convert_from = 'CAMERA' 769 | ref['vectrans'].convert_to = 'WORLD' 770 | ref['vectrans'].vector_type = 'NORMAL' 771 | 772 | # output node 773 | output = nodes.new("NodeGroupOutput") 774 | output.location = (340, -100) 775 | 776 | switches = [ 777 | ("Color Swap", -880, -600, True), 778 | ("Flat Gradient", -40, -100, True), 779 | ("Radial Linear", -420, -40, False), 780 | ("Window Global", -1450, -560, False), 781 | ] 782 | 783 | for name, locx, locy, clamp in switches: 784 | switch = nodes.new("ShaderNodeMix") 785 | switch.location = (locx, locy) 786 | switch.name = name 787 | switch.label = name 788 | switch.clamp_factor = clamp 789 | switch.inputs[0].default_value = -1 790 | switch.inputs[2].default_value = 1 791 | switch.inputs[3].default_value = 2 792 | for input in switch.inputs: 793 | input.hide = True 794 | 795 | # connect nodes 796 | link(group.outputs[0], scene.world.node_tree.nodes['Background'].inputs[0]) 797 | # connect group nodes 798 | link = BG_group.links.new 799 | node_links = { 800 | 'radialloc': [('mapsphere', 1)], 801 | 'radialscale': [('mapsphere', 3)], 802 | 'colgradient': [('flat2gradient', 2)], 803 | 'divide': [('maplinear3d', 0)], 804 | 'gradlinear': [('ramplinear', 0), ('linear2ease', 2)], 805 | 'gradsphere': [('invert', 1)], 806 | 'gradscale': [('power', 0)], 807 | 'invert': [('rampradial', 0)], 808 | 'linear2ease': [('radial2linear', 2)], 809 | 'maplinear': [('window3d', 1)], 810 | 'maplinear3d': [('window3d', 2)], 811 | 'mapsphere': [('gradsphere', 0)], 812 | 'power': [('divide', 2)], 813 | 'radial2linear': [('colgradient', 0)], 814 | 'ramplinear': [('linear2ease', 3)], 815 | 'rampradial': [('radial2linear', 1)], 816 | 'col1': [('swapcol1', 2), ('swapcol2', 1)], 817 | 'col2': [('swapcol1', 1), ('swapcol2', 2)], 818 | 'swapcol1': [('colgradient', 1), ('flat2gradient', 1)], 819 | 'swapcol2': [('colgradient', 2)], 820 | 'vectrans': [('divide', 1)], 821 | 'window3d': [('gradlinear', 0)], 822 | } 823 | 824 | for name, targets in node_links.items(): 825 | for target, input_index in targets: 826 | link(ref[name].outputs[0], ref[target].inputs[input_index]) 827 | 828 | # remaining links 829 | link(ref['flat2gradient'].outputs[0], output.inputs[0]) 830 | link(ref['texcoord2'].outputs[4], ref['vectrans'].inputs[0]) 831 | link(ref['texcoord2'].outputs[5], ref['mapsphere'].inputs[0]) 832 | link(ref['texcoord2'].outputs[5], ref['maplinear'].inputs[0]) 833 | 834 | switch_links = { 835 | 'Color Swap': ['swapcol1', 'swapcol2'], 836 | 'Flat Gradient': ['flat2gradient'], 837 | 'Radial Linear': ['radial2linear'], 838 | 'Window Global': ['linear2ease', 'window3d'], 839 | } 840 | for name, targets in switch_links.items(): 841 | for target in targets: 842 | link(nodes[name].outputs[0], ref[target].inputs[0]) 843 | 844 | # check for FuzzyFloor and set Fuzzy BG node group 845 | obj = bpy.data.objects 846 | if 'FuzzyFloor' in obj: 847 | tree = bpy.data.materials['floor_shadow'].node_tree 848 | floor_group = tree.nodes['Floor Group'] 849 | floor_alpha = tree.nodes['Floor Alpha'] 850 | floor_group.node_tree = BG_group 851 | tree.links.new(floor_group.outputs[0], floor_alpha.inputs[2]) 852 | floor_alpha.inputs[0].default_value = 1.0 853 | 854 | self.report({'INFO'}, "World 'Fuzzy World' created") 855 | return {'FINISHED'} 856 | 857 | 858 | # ------------------------------------------------------------------------ 859 | # OPERATOR - Sun 860 | # ------------------------------------------------------------------------ 861 | 862 | class OBJECT_OT_fuzzy_sun(Operator): 863 | """Place an optimized sun light. 864 | Delete the default light""" 865 | bl_idname = "object.fuzzy_sun" 866 | bl_label = "Build Sun" 867 | bl_options = {'UNDO'} 868 | 869 | @classmethod 870 | def poll(cls, context): 871 | return context.mode == 'OBJECT' 872 | 873 | def execute(self, context): 874 | scene = context.scene 875 | objects = scene.objects 876 | objs = bpy.data.objects 877 | 878 | # list of current Sun lights 879 | suns = [obj for obj in objects if obj.type == 'LIGHT' and obj.name.startswith('Sun')] 880 | 881 | if 'Light' in objects: 882 | objs.remove(objs["Light"]) 883 | 884 | # add sun light 885 | bpy.ops.object.light_add( 886 | type='SUN', align='WORLD', location=(20+len(suns), -10, 10)) 887 | 888 | # name new light 'Sun' 889 | ob = context.active_object 890 | if len(suns) == 0: 891 | ob.name = "Sun" 892 | else: 893 | v = str(len(suns)).zfill(3) 894 | ob.name = f"Sun.{v}" 895 | ob.data.name = ob.name 896 | 897 | # create collection 'Set' if it doesn't exist yet 898 | link_to_name = 'Set' 899 | try: 900 | link_to = scene.collection.children[link_to_name] 901 | except KeyError: 902 | link_to = bpy.data.collections.new(link_to_name) 903 | scene.collection.children.link(link_to) 904 | # link new light to collection 'Set' 905 | oldcoll = ob.users_collection[0].name 906 | if oldcoll == 'Scene Collection': 907 | context.collection.objects.unlink(ob) 908 | else: 909 | bpy.data.collections[oldcoll].objects.unlink(ob) 910 | bpy.data.collections['Set'].objects.link(ob) 911 | 912 | # light rotation 913 | ob.rotation_euler = (radians(50), 0, radians(40)) 914 | 915 | # light settings 916 | ob.data.energy = 1.5 917 | ob.data.angle = radians(15) 918 | 919 | ## EEVEE NEXT 920 | if is_next_version(): 921 | ob.data.use_shadow_jitter = True 922 | else: 923 | ob.data.use_contact_shadow = True 924 | 925 | # make new Sun active 926 | objects = context.view_layer.objects 927 | try: 928 | if ob.name in objects: 929 | objects.active = ob 930 | except RuntimeError: 931 | pass 932 | 933 | self.report({'INFO'}, f"'{ob.name}' added to scene") 934 | return {'FINISHED'} 935 | 936 | 937 | # ------------------------------------------------------------------------ 938 | # OPERATOR - Rim Light 939 | # ------------------------------------------------------------------------ 940 | 941 | class OBJECT_OT_fuzzy_rimlight(Operator): 942 | """Place an optimized rim light""" 943 | bl_idname = "object.fuzzy_rimlight" 944 | bl_label = "Build Rim Light" 945 | bl_options = {'UNDO'} 946 | 947 | @classmethod 948 | def poll(cls, context): 949 | return context.mode == 'OBJECT' 950 | 951 | def execute(self, context): 952 | scene = context.scene 953 | objects = scene.objects 954 | objs = bpy.data.objects 955 | 956 | # list of current rim lights 957 | rimlights = [obj for obj in objects if obj.type == 'LIGHT' and obj.name.startswith('RimLight')] 958 | 959 | # add sun light 960 | bpy.ops.object.light_add( 961 | type='SUN', align='WORLD', location=(-20-len(rimlights), 10, 10)) 962 | 963 | # name new light 'RimLight' 964 | ob = context.active_object 965 | if len(rimlights) == 0: 966 | ob.name = "RimLight" 967 | else: 968 | v = str(len(rimlights)).zfill(3) 969 | ob.name = f"RimLight.{v}" 970 | ob.data.name = ob.name 971 | 972 | # create collection 'Set' if it doesn't exist yet 973 | link_to_name = 'Set' 974 | link_to = scene.collection.children.get(link_to_name) 975 | if link_to is None: 976 | link_to = bpy.data.collections.new(link_to_name) 977 | scene.collection.children.link(link_to) 978 | 979 | oldcoll = ob.users_collection[0].name 980 | if oldcoll == 'Scene Collection': 981 | context.collection.objects.unlink(ob) 982 | else: 983 | bpy.data.collections[oldcoll].objects.unlink(ob) 984 | bpy.data.collections['Set'].objects.link(ob) 985 | 986 | # light rotation 987 | ob.rotation_euler = (radians(70), 0, radians(-150)) 988 | 989 | # light settings 990 | ob.data.energy = 10 991 | ob.data.specular_factor = 0.1 992 | ob.data.angle = radians(10) 993 | 994 | ## EEVEE NEXT 995 | if is_next_version(): 996 | ob.data.use_shadow_jitter = True 997 | else: 998 | ob.data.use_contact_shadow = True 999 | 1000 | # make new Rim Light active 1001 | objects = context.view_layer.objects 1002 | try: 1003 | if ob.name in objects: 1004 | objects.active = ob 1005 | except RuntimeError: 1006 | pass 1007 | 1008 | self.report({'INFO'}, f"'{ob.name}' added to scene") 1009 | return {'FINISHED'} 1010 | 1011 | 1012 | # ------------------------------------------------------------------------ 1013 | # OPERATOR - EEVEE optimizing 1014 | # ------------------------------------------------------------------------ 1015 | 1016 | class SCENE_OT_fuzzy_eevee(Operator): 1017 | """Set the render engine to EEVEE and optimize render settings. 1018 | AO and Bloom for Legacy, Raytracing for Next, Color Management for both, and more""" 1019 | bl_idname = "scene.fuzzy_eevee" 1020 | bl_label = "Optimize Eevee" 1021 | bl_options = {'UNDO'} 1022 | 1023 | @classmethod 1024 | def poll(cls, context): 1025 | return context.mode == 'OBJECT' 1026 | 1027 | def execute(self, context): 1028 | scene = context.scene 1029 | render = scene.render 1030 | eevee = scene.eevee 1031 | view = scene.view_settings 1032 | space = context.space_data 1033 | 1034 | # EEVEE RENDER PROPERTIES 1035 | if is_next_version(): 1036 | render.engine = 'BLENDER_EEVEE_NEXT' 1037 | version = render 1038 | else: 1039 | render.engine = 'BLENDER_EEVEE' 1040 | version = eevee 1041 | 1042 | ## GENERAL 1043 | # depth of field 1044 | eevee.bokeh_max_size = 3 1045 | eevee.use_bokeh_jittered = True 1046 | # hair 1047 | render.hair_type = 'STRIP' 1048 | # color management 1049 | view.view_transform = 'Filmic' 1050 | view.exposure = 2.0 1051 | view.gamma = 0.5 1052 | # overlay 1053 | space.shading.use_scene_world = True 1054 | space.overlay.show_look_dev = True 1055 | 1056 | ## EEVEE LEGACY 1057 | if version == eevee: 1058 | # ambient occlusion 1059 | eevee.use_gtao = True 1060 | eevee.gtao_distance = 1.6 1061 | eevee.gtao_factor = 0.7 1062 | eevee.use_gtao_bent_normals = False 1063 | # bloom 1064 | eevee.use_bloom = True 1065 | eevee.bloom_threshold = 1.0 1066 | eevee.bloom_radius = 5 1067 | # screen space reflection 1068 | eevee.use_ssr_refraction = True 1069 | eevee.use_ssr_halfres = False 1070 | eevee.ssr_quality = 1 1071 | # motion blur 1072 | version.motion_blur_position = 'START' # other options will crash blender with animated motion blur 1073 | # shadow 1074 | eevee.shadow_cascade_size = '4096' 1075 | eevee.shadow_cube_size = '2048' 1076 | eevee.use_soft_shadows = True 1077 | 1078 | ## EEVEE NEXT 1079 | if version == render: 1080 | # shadows 1081 | eevee.use_shadows = True 1082 | eevee.use_shadow_jitter_viewport = True 1083 | # ray tracing 1084 | eevee.use_raytracing = True 1085 | eevee.ray_tracing_options.resolution_scale = '1' 1086 | eevee.ray_tracing_options.trace_max_roughness = 0.5 1087 | # fast GI 1088 | eevee.fast_gi_resolution = '1' 1089 | 1090 | self.report({'INFO'}, "EEVEE settings optimized") 1091 | return {'FINISHED'} 1092 | 1093 | 1094 | # ------------------------------------------------------------------------ 1095 | # OPERATOR - Show/hide all Hair in viewport 1096 | # ------------------------------------------------------------------------ 1097 | 1098 | class OBJECT_OT_hair_viewport(Operator): 1099 | """Viewport hair visibility""" 1100 | bl_idname = "object.hair_viewport" 1101 | bl_label = "Show or Hide Hair" 1102 | bl_options = {'UNDO'} 1103 | 1104 | hide: bpy.props.BoolProperty() 1105 | 1106 | def execute(self, context): 1107 | # particle system modifiers 1108 | for obj in bpy.data.objects: 1109 | # Check for particle system hair modifiers 1110 | for modifier in obj.modifiers: 1111 | if (modifier.type == 'PARTICLE_SYSTEM' and 1112 | modifier.particle_system.particles.data.settings.type == 'HAIR'): 1113 | modifier.show_viewport = not self.hide 1114 | 1115 | # Check for CURVES type objects 1116 | if obj.type == 'CURVES': 1117 | obj.hide_viewport = self.hide 1118 | 1119 | return {'FINISHED'} 1120 | 1121 | 1122 | # ------------------------------------------------------------------------ 1123 | # OPERATOR - Copy passepartout of Active Camera to all cameras 1124 | # ------------------------------------------------------------------------ 1125 | 1126 | class OBJECT_OT_copy_passepartout(Operator): 1127 | """Copy the Passepartout Alpha of Active Camera to all cameras""" 1128 | bl_idname = "object.copy_passepartout" 1129 | bl_label = "Copy Passepartout" 1130 | bl_options = {'UNDO', 'INTERNAL'} 1131 | 1132 | def execute(self, context): 1133 | scene = context.scene 1134 | active_cam = scene.camera 1135 | alpha = active_cam.data.passepartout_alpha 1136 | 1137 | cams = bpy.data.cameras 1138 | 1139 | for cam in cams: 1140 | cam.passepartout_alpha = alpha 1141 | 1142 | return {'FINISHED'} 1143 | 1144 | 1145 | # ------------------------------------------------------------------------ 1146 | # OPERATOR - add Marker for Motion Blur 1147 | # ------------------------------------------------------------------------ 1148 | 1149 | class MARKER_OT_add_motionblur_marker(Operator): 1150 | """Add a marker to enable or disable motion blur at current frame""" 1151 | bl_idname = "marker.add_motionblur_marker" 1152 | bl_label = "Add Motion Blur Marker" 1153 | bl_options = {'UNDO', 'INTERNAL'} 1154 | 1155 | blur: bpy.props.StringProperty() 1156 | 1157 | def execute(self, context): 1158 | scene = context.scene 1159 | fr = scene.frame_current 1160 | marker = scene.timeline_markers 1161 | 1162 | if is_next_version(): 1163 | version = scene.render 1164 | else: 1165 | version = scene.eevee 1166 | 1167 | for m in marker: 1168 | if m.frame == fr and m.name.startswith("mblur"): 1169 | marker.remove(m) 1170 | break 1171 | 1172 | if self.blur == 'on': 1173 | marker.new('mblur_on', frame=fr) 1174 | version.use_motion_blur = True 1175 | elif self.blur == 'off': 1176 | marker.new('mblur_off', frame=fr) 1177 | version.use_motion_blur = False 1178 | 1179 | return {'FINISHED'} 1180 | 1181 | 1182 | # ------------------------------------------------------------------------ 1183 | # OPERATOR - Copy Shutter to Markers 1184 | # ------------------------------------------------------------------------ 1185 | 1186 | class MARKER_OT_shutter_to_markers(Operator): 1187 | """Copy current shutter time to selected 'mblur_on' markers""" 1188 | bl_idname = "marker.shutter_to_markers" 1189 | bl_label = "Copy Shutter to Markers" 1190 | bl_options = {'UNDO'} 1191 | 1192 | def execute(self, context): 1193 | scene = context.scene 1194 | markers = scene.timeline_markers 1195 | count = 0 1196 | 1197 | if is_next_version(): 1198 | version = scene.render 1199 | else: 1200 | version = scene.eevee 1201 | 1202 | for marker in markers: 1203 | if marker.select and marker.name.startswith("mblur_on"): 1204 | base_name = marker.name[:8] 1205 | v = round(version.motion_blur_shutter, 2) 1206 | marker.name = f"{base_name} {v}" 1207 | count += 1 1208 | 1209 | if count == 0: 1210 | self.report({'WARNING'}, "Selection of 'mblur_on' marker required") 1211 | return {'CANCELLED'} 1212 | 1213 | return {'FINISHED'} 1214 | 1215 | 1216 | # ------------------------------------------------------------------------ 1217 | # OPERATOR - Set active Camera 1218 | # ------------------------------------------------------------------------ 1219 | 1220 | class VIEW3D_OT_set_active_camera(Operator): 1221 | """Set camera as the active camera for this scene""" 1222 | bl_idname = "view3d.set_active_camera" 1223 | bl_label = "Set active camera" 1224 | bl_options = {'UNDO'} 1225 | 1226 | @classmethod 1227 | def poll(cls, context): 1228 | return context.object is not None and context.object.type == 'CAMERA' 1229 | 1230 | def execute(self, context): 1231 | cam = context.active_object 1232 | 1233 | context.scene.camera = cam 1234 | 1235 | return {'FINISHED'} 1236 | 1237 | 1238 | # ------------------------------------------------------------------------ 1239 | # OPERATOR - Camera Bind 1240 | # ------------------------------------------------------------------------ 1241 | 1242 | class MARKER_OT_camera_bind_new(Operator): 1243 | """Bind the selected camera to a marker on the current frame. 1244 | Requires an animation editor to be open""" 1245 | bl_idname = "marker.camera_bind_new" 1246 | bl_label = "Bind Camera to Marker" 1247 | bl_options = {'UNDO'} 1248 | 1249 | @classmethod 1250 | def poll(cls, context): 1251 | return context.object is not None and context.object.type == 'CAMERA' 1252 | 1253 | def execute(self, context): 1254 | screen = context.window.screen 1255 | count = 0 1256 | 1257 | for area in screen.areas: 1258 | if area.type == 'DOPESHEET_EDITOR': 1259 | for region in area.regions: 1260 | if region.type == 'WINDOW': 1261 | with context.temp_override( 1262 | window=context.window, 1263 | screen=screen, area=area, 1264 | region=region): 1265 | bpy.ops.marker.camera_bind() 1266 | count += 1 1267 | break 1268 | if count == 0: 1269 | self.report({'WARNING'}, "Open Animation Editor required") 1270 | return {'CANCELLED'} 1271 | 1272 | return {'FINISHED'} 1273 | 1274 | 1275 | # ------------------------------------------------------------------------ 1276 | # OPERATOR - Rename Camera as variant 1277 | # ------------------------------------------------------------------------ 1278 | 1279 | class OBJECT_OT_rename_camera_alphabet(Operator): 1280 | """Rename selected cameras as alphabetic variants of active camera""" 1281 | bl_idname = "object.rename_camera_alphabet" 1282 | bl_label = "Rename as Variant" 1283 | bl_options = {'UNDO'} 1284 | 1285 | @classmethod 1286 | def poll(cls, context): 1287 | # Ensure at least two cameras are selected and they are all of type 'CAMERA' 1288 | if len(context.selected_objects) < 2: 1289 | return False 1290 | cameras_selected = all(obj.type == 'CAMERA' for obj in context.selected_objects) 1291 | return cameras_selected 1292 | 1293 | def execute(self, context): 1294 | scene = context.scene 1295 | objs = bpy.data.objects 1296 | 1297 | active_cam = context.active_object 1298 | selected_cams = context.selected_objects 1299 | 1300 | # Use ord and chr to generate alphabet dynamically 1301 | ABC = [chr(i) for i in range(ord('A'), ord('Z') + 1)] 1302 | 1303 | # Get all objects of type 'CAMERA' 1304 | cams = [obj for obj in objs if obj.type == 'CAMERA'] 1305 | 1306 | # Detect if active camera name ends with a capital letter and has no numbers before it 1307 | if active_cam.name[-1:].isupper(): 1308 | if len(active_cam.name) > 1: 1309 | # Check the second last character 1310 | second_last_char = active_cam.name[-2] 1311 | if second_last_char.isupper() and not any(char.isdigit() for char in active_cam.name[:-1]): 1312 | # If second last character is uppercase and no digits, trigger the error 1313 | self.report({'ERROR'}, 1314 | 'Naming convention not valid. Use number or single upper case as suffix') 1315 | return {'CANCELLED'} 1316 | 1317 | if active_cam in cams: 1318 | base_name = active_cam.name.rstrip('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 1319 | last_char = active_cam.name[len(base_name):] 1320 | 1321 | # If the last character is alphabetic, we will increment from that point 1322 | if last_char and last_char[-1:].isalpha(): 1323 | base_name = active_cam.name[:-1] 1324 | 1325 | for cam in selected_cams: 1326 | if cam != active_cam: 1327 | for letter in ABC: 1328 | name_ABC = f"{base_name}{letter}" 1329 | # Check if the new name already exists among cameras 1330 | if not any(existing_cam.name == name_ABC for existing_cam in cams): 1331 | cam.name = name_ABC 1332 | break 1333 | 1334 | return {'FINISHED'} 1335 | 1336 | 1337 | # ------------------------------------------------------------------------ 1338 | # OPERATOR - Move Keyframes and Markers 1339 | # ------------------------------------------------------------------------ 1340 | 1341 | class TRANSFORM_OT_keyframes_markers(Operator): 1342 | """Move keyframes and markers from current frame, regardless of selection or visibility""" 1343 | bl_idname = "transform.keyframes_markers" 1344 | bl_label = "Move Keyframes and Markers" 1345 | bl_options = {'REGISTER', 'UNDO'} 1346 | 1347 | frame_shift: IntProperty( 1348 | name="Frames", 1349 | description="Amount of frames to move", 1350 | default=0, 1351 | options={'SKIP_SAVE'} 1352 | ) 1353 | 1354 | before_current: BoolProperty( 1355 | name="Before Current Frame", 1356 | description="Move before current frame instead of after", 1357 | default=False, 1358 | options={'SKIP_SAVE'} 1359 | ) 1360 | 1361 | keys: BoolProperty( 1362 | name="Keyframes", 1363 | description="Move Keyframes (NOT linked actions and locked curves)", 1364 | default=True 1365 | ) 1366 | 1367 | markers: BoolProperty( 1368 | name="Markers", 1369 | description="Move Markers", 1370 | default=True 1371 | ) 1372 | 1373 | fake_user: BoolProperty( 1374 | name="Fake User", 1375 | description="Include actions with Fake User", 1376 | default=False 1377 | ) 1378 | 1379 | def execute(self, context): 1380 | scene = context.scene 1381 | fr = scene.frame_current 1382 | frames = self.frame_shift 1383 | a = bpy.data.actions 1384 | 1385 | if self.keys: 1386 | for action in a: 1387 | if not action.library and (self.fake_user or not action.use_fake_user): 1388 | fcurves = action.fcurves 1389 | for curve in fcurves: 1390 | if not curve.lock: # Check if the curve is not locked 1391 | kfp = curve.keyframe_points 1392 | for point in kfp: 1393 | if not self.before_current and point.co.x > fr: 1394 | point.co_ui.x += frames 1395 | elif self.before_current and point.co.x < fr: 1396 | point.co_ui.x += frames 1397 | 1398 | if self.markers: 1399 | m = scene.timeline_markers 1400 | for marker in m: 1401 | if self.before_current == False: 1402 | if marker.frame > fr: 1403 | marker.frame += frames 1404 | else: 1405 | if marker.frame < fr: 1406 | marker.frame += frames 1407 | 1408 | return {'FINISHED'} 1409 | 1410 | def invoke(self, context, event): 1411 | return context.window_manager.invoke_props_popup(self, event) 1412 | 1413 | def draw(self, context): 1414 | layout = self.layout 1415 | layout.use_property_split = True 1416 | layout.prop(self, 'frame_shift') 1417 | layout.prop(self, 'before_current') 1418 | layout.separator(factor=0.5) 1419 | row = layout.row(heading="Target") 1420 | row.prop(self, 'keys') 1421 | row.prop(self, 'markers') 1422 | row = layout.row() 1423 | if not self.keys: 1424 | row.enabled = False 1425 | row.prop(self, 'fake_user') 1426 | layout.separator(factor=0.5) 1427 | layout.label(icon='INFO', text="Target is regardless of selection or visibility") 1428 | 1429 | 1430 | # ------------------------------------------------------------------------ 1431 | # OPERATOR - add light parent for Sun, Rimlight and FloorNormal 1432 | # ------------------------------------------------------------------------ 1433 | 1434 | class OBJECT_OT_light_parent(Operator): 1435 | """Add empty as parent for Sun, RimLight and FloorNormal""" 1436 | bl_idname = "object.light_parent" 1437 | bl_label = "Add Light Parent" 1438 | bl_options = {'UNDO'} 1439 | 1440 | @classmethod 1441 | def poll(cls, context): 1442 | objs = context.scene.objects 1443 | return context.mode == 'OBJECT' and 'LightParent' not in objs 1444 | 1445 | def execute(self, context): 1446 | objs = bpy.data.objects 1447 | 1448 | # create empty 1449 | light_parent = objs.new('LightParent', None) 1450 | light_parent.empty_display_type = 'SPHERE' 1451 | light_parent.empty_display_size = 3 1452 | 1453 | # add empty to collection 'Set' 1454 | colls = bpy.data.collections 1455 | set = colls.get('Set') 1456 | if set: 1457 | coll = set 1458 | else: 1459 | coll = context.scene.collection 1460 | 1461 | coll.objects.link(light_parent) 1462 | 1463 | sun = objs.get('Sun') 1464 | rim = objs.get('RimLight') 1465 | normal = objs.get('FloorNormal') 1466 | 1467 | for item in [sun, rim, normal]: 1468 | if item: 1469 | item.parent = light_parent 1470 | 1471 | self.report({'INFO'}, f"'LightParent' added to collection {coll.name}") 1472 | return {'FINISHED'} 1473 | 1474 | 1475 | # ------------------------------------------------------------------------ 1476 | # OPERATOR - rotate LightParent or HDRI according to Active Camera 1477 | # ------------------------------------------------------------------------ 1478 | 1479 | class OBJECT_OT_rotate_lighting(Operator): 1480 | """Match Z-rotation of lighting to active camera. 1481 | If target is HDRI, rotation is applied to secondary rotation""" 1482 | bl_idname = "object.rotate_lighting" 1483 | bl_label = "Rotate Lighting" 1484 | bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} 1485 | 1486 | @classmethod 1487 | def poll(cls, context): 1488 | objs = context.scene.objects 1489 | return context.mode == 'OBJECT' 1490 | 1491 | hdri: BoolProperty( 1492 | name="Include HDRI", 1493 | description="Rotate HDRI", 1494 | options={'SKIP_SAVE', 'HIDDEN'} 1495 | ) 1496 | 1497 | parent: BoolProperty( 1498 | name="Include LightParent", 1499 | description="Rotate LightParent", 1500 | options={'SKIP_SAVE', 'HIDDEN'} 1501 | ) 1502 | 1503 | def execute(self, context): 1504 | scene = context.scene 1505 | fr = scene.frame_current 1506 | 1507 | # active camera Z rotation 1508 | cam_rot = scene.camera.rotation_euler[2] 1509 | 1510 | if self.parent: 1511 | # get LightParent and apply Z rotation 1512 | parent = scene.objects.get('LightParent') 1513 | if parent: 1514 | parent.rotation_euler[2] = cam_rot 1515 | 1516 | if self.hdri: 1517 | # find HDRI and apply Z rotation 1518 | if scene.world.name == 'Fuzzy World': 1519 | nodes = scene.world.node_tree.nodes 1520 | HDRI = nodes.get("HDRI Delta Rot") 1521 | if HDRI: 1522 | HDRI.inputs[2].default_value[2] = cam_rot * -1 + radians(90) 1523 | 1524 | if self.parent: 1525 | target = "LightParent" 1526 | elif self.hdri: 1527 | target = "HDRI" 1528 | self.report({'INFO'}, f"{target} rotated on Z-axis") 1529 | return {'FINISHED'} 1530 | 1531 | 1532 | # ------------------------------------------------------------------------ 1533 | # PANELS - Scene Builder 1534 | # ------------------------------------------------------------------------ 1535 | 1536 | class BuildScenePanel(Panel): 1537 | bl_label = "Scene Builder" 1538 | bl_idname = "VIEW3D_PT_build_scene" 1539 | bl_space_type = 'VIEW_3D' 1540 | bl_region_type = 'UI' 1541 | bl_category = 'Fuzzy' 1542 | bl_options = {'DEFAULT_CLOSED'} 1543 | bl_order = 0 1544 | 1545 | @classmethod 1546 | def poll(cls, context): 1547 | return context.mode == 'OBJECT' 1548 | 1549 | def draw(self, context): 1550 | pass 1551 | 1552 | 1553 | class BuildSceneChild: 1554 | bl_space_type = 'VIEW_3D' 1555 | bl_region_type = 'UI' 1556 | bl_parent_id = "VIEW3D_PT_build_scene" 1557 | 1558 | 1559 | class BuildAllPanel(BuildSceneChild, Panel): 1560 | bl_label = "Build All" 1561 | bl_idname = "VIEW3D_PT_build_all" 1562 | 1563 | def draw(self, context): 1564 | layout = self.layout 1565 | 1566 | row = layout.row() 1567 | row.scale_y = 2 1568 | row.operator("scene.build_all", text='POP!', icon='SHADERFX') 1569 | 1570 | 1571 | class BuildPartsPanel(BuildSceneChild, Panel): 1572 | bl_label = "Build in Parts" 1573 | bl_idname = "VIEW3D_PT_build_parts" 1574 | bl_options = {'DEFAULT_CLOSED'} 1575 | 1576 | def draw(self, context): 1577 | layout = self.layout 1578 | 1579 | row = layout.row(align=True) 1580 | row.scale_y = 1.2 1581 | row.operator("object.fuzzy_camera", text='Camera', icon='CAMERA_DATA') 1582 | row.operator("mesh.fuzzy_floor", text="Floor", icon='GRID') 1583 | 1584 | col = layout.column(align=True) 1585 | col.scale_y = 1.2 1586 | row = col.row(align=True) 1587 | row.operator("world.fuzzy_sky", text="Sky", icon='MAT_SPHERE_SKY') 1588 | row.operator("object.fuzzy_sun", text="Sun", icon='LIGHT_SUN') 1589 | col.operator("object.fuzzy_rimlight", text="Rim Light", icon='LIGHT') 1590 | col.operator("scene.fuzzy_eevee", text="Optimize EEVEE", icon='CAMERA_STEREO') 1591 | 1592 | 1593 | class BackgroundPanel(BuildSceneChild, Panel): 1594 | bl_label = "Background" 1595 | bl_idname = "VIEW3D_PT_background" 1596 | bl_options = {'DEFAULT_CLOSED'} 1597 | 1598 | @classmethod 1599 | def poll(cls, context): 1600 | scene = context.scene.world 1601 | if scene is not None: 1602 | return scene.name == "Fuzzy World" 1603 | 1604 | def draw(self, context): 1605 | scene = context.scene 1606 | fuzzyprops = scene.fuzzy_props 1607 | group_node = scene.world.node_tree.nodes 1608 | node = group_node['BG Group'].node_tree.nodes 1609 | 1610 | layout = self.layout 1611 | col = layout.column() 1612 | col.use_property_split = True 1613 | col.use_property_decorate = False 1614 | row = col.row(align=True) 1615 | row.prop(fuzzyprops, "fuzzy_color1", text='Palette') 1616 | row.prop(fuzzyprops, "fuzzy_color2", text='') 1617 | row.separator() 1618 | row.label(icon='BLANK1') 1619 | row = col.row(align=True) 1620 | swap = node.get('Color Swap') 1621 | if swap: 1622 | row.prop(node['BG Color 1' if swap.clamp_factor else 'BG Color 2'].outputs[0], 'default_value', text='Sky Colors') 1623 | row.prop(node['BG Color 2' if swap.clamp_factor else 'BG Color 1'].outputs[0], 'default_value', text='') 1624 | row.separator() 1625 | row.prop(swap, 'clamp_factor', icon='FILE_REFRESH', icon_only=True, emboss=False) 1626 | 1627 | flat_grad = node.get('Flat Gradient') 1628 | rad_lin = node.get('Radial Linear') 1629 | win_glob = node.get('Window Global') 1630 | if flat_grad and rad_lin and win_glob: 1631 | col = layout.column(align=True) 1632 | col.prop(flat_grad, 'clamp_factor', text='Gradient') 1633 | 1634 | col = col.column(align=True) 1635 | col.enabled = flat_grad.clamp_factor 1636 | row = col.row(align=True) 1637 | row.prop(rad_lin, 'clamp_factor', text='Radial', toggle=1, invert_checkbox=True) 1638 | row.prop(rad_lin, 'clamp_factor', text='Linear', toggle=1) 1639 | row = row.row(align=True) 1640 | row.enabled = rad_lin.clamp_factor 1641 | row.prop(win_glob, 'clamp_factor', text='', icon='WORLD') 1642 | 1643 | col = col.column(align=True) 1644 | if rad_lin.clamp_factor and win_glob.clamp_factor: 1645 | scale_grad = node.get('Scale Gradient') 1646 | if scale_grad: 1647 | col.separator(factor=0.5) 1648 | col.prop(scale_grad.inputs[0], "default_value", text='Scale from Horizon') 1649 | elif not rad_lin.clamp_factor: 1650 | col.separator(factor=0.5) 1651 | col.use_property_split = True 1652 | col.use_property_decorate = False 1653 | 1654 | rad_loc = node.get('Radial Location') 1655 | if rad_loc: 1656 | row = col.row(align=True) 1657 | row.prop(rad_loc.inputs[0], "default_value", text="Loc XY", index=0) 1658 | row.prop(rad_loc.inputs[0], "default_value", text="", index=1) 1659 | 1660 | rad_scale = node.get('Radial Scale') 1661 | if rad_scale: 1662 | row = col.row(align=True) 1663 | row.prop(rad_scale.inputs[0], "default_value", text="Scale XY", index=0) 1664 | row.prop(rad_scale.inputs[0], "default_value", text="", index=1) 1665 | 1666 | layout.prop(scene.render, "film_transparent") 1667 | 1668 | 1669 | class HDRIPanel(BuildSceneChild, Panel): 1670 | bl_label = "Lighting" 1671 | bl_idname = "VIEW3D_PT_hdri" 1672 | bl_options = {'DEFAULT_CLOSED'} 1673 | 1674 | @classmethod 1675 | def poll(cls, context): 1676 | scene = context.scene.world 1677 | if scene is not None: 1678 | return scene.name == "Fuzzy World" 1679 | 1680 | def draw(self, context): 1681 | scene = context.scene 1682 | objs = scene.objects 1683 | nodes = scene.world.node_tree.nodes 1684 | 1685 | layout = self.layout 1686 | col = layout.column() 1687 | col.label(text="Light Parent", icon='LIGHT') 1688 | row = col.row(align=True) 1689 | row.scale_y = 1.2 1690 | parent = objs.get('LightParent') 1691 | if not parent: 1692 | row.operator('object.light_parent', text="Create", icon='SPHERE') 1693 | else: 1694 | row.operator('object.rotate_lighting', text="Rotate", icon='CON_ROTLIMIT').parent = True 1695 | row.separator(factor=0.5) 1696 | row.prop(parent, 'hide_viewport', text='', emboss=False) 1697 | 1698 | col = layout.column(align=True) 1699 | col.use_property_split = True 1700 | col.use_property_decorate = False 1701 | col.label(text="HDRI", icon='IMAGE_DATA') 1702 | HDRI_node = nodes.get("World HDRI") 1703 | if HDRI_node: 1704 | col.template_ID(HDRI_node, 'image', open='image.open', live_icon=True) 1705 | col.separator() 1706 | 1707 | hdri_rot = nodes.get("HDRI Rotation") 1708 | hdri_delta_rot = nodes.get("HDRI Delta Rot") 1709 | hdri_str = nodes.get("HDRI Strength") 1710 | if hdri_rot: 1711 | row = col.row(align=True) 1712 | row.prop(hdri_rot.inputs[2], 'default_value', index=2, text='Rotation') 1713 | if hdri_delta_rot: 1714 | row.operator('object.rotate_lighting', text="", icon='CON_ROTLIMIT').hdri = True 1715 | if hdri_str: 1716 | col.prop(hdri_str.inputs[1], 'default_value', index=-1, text='Strength') 1717 | 1718 | clamp_node = nodes.get("Clamp Reflection") 1719 | if clamp_node: 1720 | col.separator() 1721 | if context.engine == 'BLENDER_EEVEE_NEXT': 1722 | text = "Clamp" 1723 | else: 1724 | text = "Clamp Glossy" 1725 | col.prop(clamp_node.outputs[0], 'default_value', text=text) 1726 | 1727 | 1728 | class FloorPanel(BuildSceneChild, Panel): 1729 | bl_label = "Fuzzy Floor" 1730 | bl_idname = "VIEW3D_PT_floor" 1731 | bl_options = {'DEFAULT_CLOSED'} 1732 | 1733 | @classmethod 1734 | def poll(cls, context): 1735 | scene = context.scene 1736 | objects = scene.objects 1737 | engines = ['BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'] 1738 | return 'FuzzyFloor' in objects and context.engine in engines 1739 | 1740 | def draw(self, context): 1741 | scene = context.scene 1742 | layout = self.layout 1743 | layout.use_property_split = True 1744 | layout.use_property_decorate = False 1745 | 1746 | floor = bpy.data.objects.get('FuzzyFloor') 1747 | if floor: 1748 | mod = floor.modifiers.get('NormalDirection') 1749 | mat = bpy.data.materials.get('floor_shadow') 1750 | if mat and mat.node_tree: 1751 | nodes = mat.node_tree.nodes 1752 | val = 'default_value' 1753 | 1754 | # Check Blender version 4.2 or above 1755 | if is_next_version(): 1756 | col = layout.column(align=True) 1757 | ao_node = nodes.get('AO') 1758 | ao_factor_node = nodes.get('AO Factor') 1759 | if ao_node: 1760 | col.prop(ao_node.inputs[1], val, text="AO Distance") 1761 | if ao_factor_node: 1762 | col.prop(ao_factor_node.inputs[0], val, text="AO Factor") 1763 | col.prop(mat, 'surface_render_method', text="Method") 1764 | if mat.surface_render_method == 'DITHERED': 1765 | col.prop(mat, 'use_raytrace_refraction') 1766 | 1767 | clamp_node = nodes.get('Clamp Value') 1768 | dodge_node = nodes.get('Dodge Value') 1769 | shadow_node = nodes.get('Shadow Value') 1770 | col = layout.column(align=True) 1771 | 1772 | if clamp_node: 1773 | col.prop(clamp_node.inputs[0], val, text="Clamp Dark") 1774 | if dodge_node: 1775 | col.prop(dodge_node.inputs[0], val, text="Dodge Bright") 1776 | if shadow_node: 1777 | layout.prop(shadow_node.inputs[0], val, text="Value Fix") 1778 | 1779 | fuzzy_bg = bpy.data.node_groups.get('Fuzzy BG') 1780 | if fuzzy_bg: 1781 | col = layout.column(heading="Floor") 1782 | floor_alpha_node = nodes.get('Floor Alpha') 1783 | if floor_alpha_node: 1784 | col.prop(floor_alpha_node, 'mute', text="Holdout") 1785 | 1786 | if mod: 1787 | split = layout.split(factor=0.4) 1788 | split.alignment = 'RIGHT' 1789 | split.label(text='Normal Edit') 1790 | row = split.row(align=True) 1791 | row.scale_x = 1.3 1792 | row.prop(mod, 'show_viewport', text="") 1793 | row.prop(mod, 'show_render', text="") 1794 | 1795 | 1796 | # ------------------------------------------------------------------------ 1797 | # PANELS - Fuzzy View 1798 | # ------------------------------------------------------------------------ 1799 | 1800 | class VIEW3D_PT_viewport(Panel): 1801 | bl_label = "Fuzzy View" 1802 | bl_space_type = 'VIEW_3D' 1803 | bl_region_type = 'UI' 1804 | bl_category = 'Fuzzy' 1805 | bl_options = {'DEFAULT_CLOSED'} 1806 | bl_order = 2 1807 | 1808 | def draw_header_preset(self, context): 1809 | layout = self.layout 1810 | layout.scale_x = 0.92 1811 | layout.prop(context.space_data, "lens", text="") 1812 | 1813 | def draw(self, context): 1814 | pass 1815 | 1816 | 1817 | class ViewportChild: 1818 | bl_space_type = 'VIEW_3D' 1819 | bl_region_type = 'UI' 1820 | bl_parent_id = 'VIEW3D_PT_viewport' 1821 | 1822 | 1823 | class VIEW3D_PT_simplify(ViewportChild, Panel): 1824 | bl_label = "Simplify" 1825 | 1826 | def draw_header(self, context): 1827 | rd = context.scene.render 1828 | self.layout.prop(rd, "use_simplify", text="") 1829 | 1830 | def draw(self, context): 1831 | layout = self.layout 1832 | rd = context.scene.render 1833 | layout.use_property_split = True 1834 | layout.use_property_decorate = False 1835 | 1836 | layout.active = rd.use_simplify 1837 | 1838 | flow = layout.grid_flow() 1839 | 1840 | col = flow.column() 1841 | col.prop(rd, "simplify_subdivision", text="Subdivision") 1842 | 1843 | col = flow.column() 1844 | col.prop(rd, "simplify_child_particles", text="Hair") 1845 | 1846 | 1847 | class VIEW3D_PT_hair(ViewportChild, Panel): 1848 | bl_label = "Hair" 1849 | bl_options = {'DEFAULT_CLOSED'} 1850 | 1851 | def draw(self, context): 1852 | layout = self.layout 1853 | scene = context.scene 1854 | rd = scene.render 1855 | 1856 | row = layout.row(align=True) 1857 | row.use_property_split = True 1858 | row.use_property_decorate = False 1859 | row.prop(rd, "hair_type", text = "Type", expand=True) 1860 | 1861 | col = layout.column(align=True) 1862 | row = col.row(align=True) 1863 | row.label(text="Visibility") 1864 | row = col.row(align=True) 1865 | row.operator("object.hair_viewport", text="Show All", icon='HIDE_OFF').hide = False 1866 | row.operator("object.hair_viewport", text="Hide All", icon='HIDE_ON').hide = True 1867 | 1868 | 1869 | class VIEW3D_PT_miscellaneous(ViewportChild, Panel): 1870 | bl_label = "Viewport Display" 1871 | bl_options = {'DEFAULT_CLOSED'} 1872 | 1873 | def draw(self, context): 1874 | space = context.space_data 1875 | overlay = space.overlay 1876 | shade = space.shading 1877 | object = context.object 1878 | 1879 | layout = self.layout 1880 | split = layout.split(factor=0.25) 1881 | split.alignment = 'RIGHT' 1882 | split.label(text='Local') 1883 | col = split.column(align=True) 1884 | col.prop(overlay, "show_relationship_lines") 1885 | col.prop(shade, "show_backface_culling") 1886 | if object is not None: 1887 | split = layout.split(factor=0.25) 1888 | split.alignment = 'RIGHT' 1889 | split.label(text='Object') 1890 | split.prop(object, "show_in_front", text="Show in Front") 1891 | 1892 | 1893 | # ------------------------------------------------------------------------ 1894 | # PANELS - Camera Control 1895 | # ------------------------------------------------------------------------ 1896 | 1897 | class VIEW3D_PT_cameras(Panel): 1898 | bl_label = " Camera Control" 1899 | bl_space_type = 'VIEW_3D' 1900 | bl_region_type = 'UI' 1901 | bl_category = 'Fuzzy' 1902 | bl_options = {'DEFAULT_CLOSED'} 1903 | bl_order = 3 1904 | 1905 | def draw_header_preset(self, context): 1906 | layout = self.layout 1907 | if context.space_data.lock_camera: 1908 | icon = 'LOCKED' 1909 | else: 1910 | icon = 'UNLOCKED' 1911 | row = layout.row(align=True) 1912 | if context.space_data.lock_camera == True: 1913 | row.alert = True 1914 | row.prop(context.space_data, 'lock_camera', text='', icon=icon, emboss=False) 1915 | 1916 | colls = bpy.data.collections 1917 | cam_coll = colls.get('Cameras') 1918 | if cam_coll: 1919 | row.separator(factor=0.5) 1920 | row.alert = False 1921 | row.prop(cam_coll, 'hide_viewport', text="", icon='RESTRICT_VIEW_OFF', emboss=False) 1922 | 1923 | def draw(self, context): 1924 | pass 1925 | 1926 | 1927 | class VIEW3D_PT_camera_scene(Panel): 1928 | bl_label = "Scene" 1929 | bl_space_type = 'VIEW_3D' 1930 | bl_region_type = 'UI' 1931 | bl_parent_id = 'VIEW3D_PT_cameras' 1932 | bl_options = {'DEFAULT_CLOSED'} 1933 | 1934 | def draw_header_preset(self, context): 1935 | layout = self.layout 1936 | scene = context.scene 1937 | active_obj = context.view_layer.objects.active 1938 | 1939 | if scene.camera and active_obj != scene.camera and context.mode == 'OBJECT': 1940 | if scene.camera.name not in context.view_layer.objects: 1941 | layout.enabled = False 1942 | layout.operator("object.select_camera", text="", 1943 | icon='RESTRICT_SELECT_OFF', emboss=False) 1944 | 1945 | def draw(self, context): 1946 | scene = context.scene 1947 | layout = self.layout 1948 | 1949 | if context.mode == 'OBJECT': 1950 | col = layout.column() 1951 | col.scale_y = 1.2 1952 | col.operator("object.fuzzy_camera", text="Build", icon='CAMERA_DATA') 1953 | 1954 | # subpanel for blender 4.1 or higher, else box 1955 | bl_version = bpy.app.version 1956 | if bl_version >= (4, 1, 0): 1957 | header, panel = layout.panel("VIEW3D_PT_camera_scene", default_closed=True) 1958 | row = header.row(align=True) 1959 | sub = panel 1960 | else: 1961 | box = layout.box() 1962 | col = box.column(align=True) 1963 | col.label(text='Active:') 1964 | row = col.row(align=True) 1965 | sub = box 1966 | 1967 | row.prop(scene, "camera", text="", icon='CAMERA_DATA') 1968 | row.operator('view3d.view_camera', text='', icon='VIEWZOOM') 1969 | row.operator('view3d.camera_to_view', text='', icon='DECORATE_OVERRIDE') 1970 | 1971 | if sub and scene.camera is not None: 1972 | container = panel if bl_version >= (4, 1, 0) else box 1973 | col = container.column(align=True) 1974 | # camera properties 1975 | cam = scene.camera.data 1976 | row = col.row(align=True) 1977 | row.prop(cam, 'lens') 1978 | row = col.row(align=True) 1979 | row.prop(cam, 'clip_start', text="Start") 1980 | row.prop(cam, 'clip_end', text="End") 1981 | col.separator() 1982 | row = col.row(align=True) 1983 | row.prop(cam, 'passepartout_alpha', text="Passepartout") 1984 | row.operator('object.copy_passepartout', text='', icon='DUPLICATE') 1985 | 1986 | # Motion Blur - check render engine 1987 | if context.engine == 'BLENDER_EEVEE_NEXT' or context.engine == 'CYCLES': 1988 | version = scene.render 1989 | elif context.engine == 'BLENDER_EEVEE': 1990 | version = scene.eevee 1991 | col = layout.column(align=True, heading="Motion Blur") 1992 | col.use_property_split = True 1993 | col.use_property_decorate = False 1994 | col.prop(version, 'use_motion_blur', text="") 1995 | col.prop(version, 'motion_blur_shutter') 1996 | 1997 | # Motion Blur Markers 1998 | col.separator(factor=0.5) 1999 | split = col.split(factor=0.4) 2000 | split.scale_y = 1.2 2001 | row = split.row() 2002 | row.scale_x = 1.1 2003 | row.alignment = 'RIGHT' 2004 | row.label(text="Marker") 2005 | # check for 'mblur' marker 2006 | markers = scene.timeline_markers 2007 | if markers is not None: 2008 | for m in markers: 2009 | if m.name.startswith('mblur'): 2010 | fuzzyprops = scene.fuzzy_props 2011 | row.prop(fuzzyprops, 'scene_animate', text="", icon='ACTION') 2012 | break 2013 | row = split.row(align=True) 2014 | row.operator('marker.add_motionblur_marker', text="On", icon='KEYFRAME_HLT').blur = 'on' 2015 | row.operator('marker.add_motionblur_marker', text="Off", icon='KEYFRAME').blur = 'off' 2016 | row.operator('marker.shutter_to_markers', text='', icon='MARKER_HLT') 2017 | 2018 | 2019 | class VIEW3D_PT_camera_selected(Panel): 2020 | bl_label = "" 2021 | bl_space_type = 'VIEW_3D' 2022 | bl_region_type = 'UI' 2023 | bl_parent_id = 'VIEW3D_PT_cameras' 2024 | bl_context = 'objectmode' 2025 | bl_options = {'DEFAULT_CLOSED'} 2026 | 2027 | @classmethod 2028 | def poll(cls, context): 2029 | return context.view_layer.objects.active is not None and context.object.type == 'CAMERA' 2030 | 2031 | def draw_header(self, context): 2032 | active_obj = context.view_layer.objects.active 2033 | if active_obj and active_obj != context.scene.camera: 2034 | self.layout.operator('view3d.set_active_camera', text=active_obj.name, icon="CAMERA_DATA") 2035 | else: 2036 | self.layout.prop(active_obj, "name", text='', emboss=False, icon="VIEW_CAMERA") 2037 | 2038 | def draw(self, context): 2039 | layout = self.layout 2040 | object = context.object 2041 | layout.use_property_split = True 2042 | layout.use_property_decorate = False 2043 | 2044 | row = layout.row(align=True) 2045 | row.scale_y = 1.2 2046 | row.operator('marker.camera_bind_new', text="Bind to Marker", icon='KEYFRAME_HLT') 2047 | row.separator(factor=0.5) 2048 | row.scale_x = 1.2 2049 | row.operator('object.rename_camera_alphabet', text='', icon='SORTALPHA') 2050 | 2051 | layout.prop(object.data, "lens") 2052 | 2053 | col = layout.column(heading="Show", align=True) 2054 | col.prop(object, "show_name", text="Name") 2055 | col.prop(object.data, "show_limits", text="Limits") 2056 | 2057 | col = layout.column(align=True) 2058 | col.prop(object.data, "clip_start", text="Clip Start") 2059 | col.prop(object.data, "clip_end", text="End") 2060 | 2061 | col = layout.column(heading="DoF") 2062 | col.prop(object.data.dof, "use_dof", text="") 2063 | if object.data.dof.use_dof: 2064 | col.prop(object.data.dof, "focus_object", text="Object") 2065 | row = col.row() 2066 | if object.data.dof.focus_object is not None: 2067 | row.enabled = False 2068 | row.prop(object.data.dof, "focus_distance", text="Distance") 2069 | row.operator('ui.eyedropper_depth', icon='EYEDROPPER', text="") 2070 | col.prop(object.data.dof, "aperture_fstop") 2071 | 2072 | 2073 | # ------------------------------------------------------------------------ 2074 | # REGISTRATION 2075 | # ------------------------------------------------------------------------ 2076 | 2077 | addon_keymaps = [] 2078 | 2079 | classes = [ 2080 | # properties 2081 | FuzzyProperties, 2082 | 2083 | # operators 2084 | SCENE_OT_build_all, 2085 | OBJECT_OT_fuzzy_camera, 2086 | MESH_OT_fuzzy_floor, 2087 | WORLD_OT_fuzzy_sky, 2088 | OBJECT_OT_fuzzy_sun, 2089 | OBJECT_OT_fuzzy_rimlight, 2090 | SCENE_OT_fuzzy_eevee, 2091 | 2092 | OBJECT_OT_hair_viewport, 2093 | 2094 | OBJECT_OT_copy_passepartout, 2095 | MARKER_OT_add_motionblur_marker, 2096 | MARKER_OT_shutter_to_markers, 2097 | 2098 | VIEW3D_OT_set_active_camera, 2099 | MARKER_OT_camera_bind_new, 2100 | OBJECT_OT_rename_camera_alphabet, 2101 | 2102 | TRANSFORM_OT_keyframes_markers, 2103 | 2104 | OBJECT_OT_light_parent, 2105 | OBJECT_OT_rotate_lighting, 2106 | 2107 | # panels 2108 | BuildScenePanel, 2109 | BuildAllPanel, 2110 | BuildPartsPanel, 2111 | BackgroundPanel, 2112 | FloorPanel, 2113 | HDRIPanel, 2114 | 2115 | VIEW3D_PT_viewport, 2116 | VIEW3D_PT_simplify, 2117 | VIEW3D_PT_hair, 2118 | VIEW3D_PT_miscellaneous, 2119 | 2120 | VIEW3D_PT_cameras, 2121 | VIEW3D_PT_camera_scene, 2122 | VIEW3D_PT_camera_selected, 2123 | 2124 | ] 2125 | 2126 | 2127 | def register(): 2128 | for cls in classes: 2129 | bpy.utils.register_class(cls) 2130 | bpy.types.Scene.fuzzy_props = PointerProperty(type=FuzzyProperties) 2131 | 2132 | bpy.app.handlers.load_post.append(reload_image) 2133 | bpy.app.handlers.load_post.append(auto_animate_scene) 2134 | bpy.app.handlers.load_post.append(disable_animate_scene) 2135 | bpy.app.handlers.load_post.append(name_fix) 2136 | 2137 | # Add hotkey Alt+M for 'Move Keyframes and Markers' 2138 | wm = bpy.context.window_manager 2139 | kc = wm.keyconfigs.addon 2140 | if kc: 2141 | km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY', region_type='WINDOW') 2142 | kmi = km.keymap_items.new(TRANSFORM_OT_keyframes_markers.bl_idname, 2143 | type='M', value='PRESS', alt=True) 2144 | addon_keymaps.append((km, kmi)) 2145 | 2146 | 2147 | def unregister(): 2148 | for cls in classes: 2149 | bpy.utils.unregister_class(cls) 2150 | del bpy.types.Scene.fuzzy_props 2151 | 2152 | bpy.app.handlers.load_post.remove(reload_image) 2153 | bpy.app.handlers.load_post.remove(auto_animate_scene) 2154 | bpy.app.handlers.load_post.remove(disable_animate_scene) 2155 | bpy.app.handlers.load_post.remove(name_fix) 2156 | 2157 | # Remove hotkey Alt+M 2158 | for km, kmi in addon_keymaps: 2159 | km.keymap_items.remove(kmi) 2160 | addon_keymaps.clear() 2161 | 2162 | 2163 | if __name__ == "__main__": 2164 | register() 2165 | -------------------------------------------------------------------------------- /images/CameraControl_Scene_v3_operators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/CameraControl_Scene_v3_operators.png -------------------------------------------------------------------------------- /images/CameraControl_SelectedCamera_v3_operators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/CameraControl_SelectedCamera_v3_operators.png -------------------------------------------------------------------------------- /images/Collection_Cameras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/Collection_Cameras.png -------------------------------------------------------------------------------- /images/Collection_Set_Floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/Collection_Set_Floor.png -------------------------------------------------------------------------------- /images/Collection_Set_RimLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/Collection_Set_RimLight.png -------------------------------------------------------------------------------- /images/Collection_Set_Sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/Collection_Set_Sun.png -------------------------------------------------------------------------------- /images/FloorHoldout_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/FloorHoldout_disabled.png -------------------------------------------------------------------------------- /images/FloorHoldout_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/FloorHoldout_enabled.png -------------------------------------------------------------------------------- /images/FuzzyView_Lens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/FuzzyView_Lens.png -------------------------------------------------------------------------------- /images/FuzzyViewport_Hair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/FuzzyViewport_Hair.png -------------------------------------------------------------------------------- /images/FuzzyViewport_Simplify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/FuzzyViewport_Simplify.png -------------------------------------------------------------------------------- /images/FuzzyViewport_ViewportDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/FuzzyViewport_ViewportDisplay.png -------------------------------------------------------------------------------- /images/ICON_EARTH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/ICON_EARTH.png -------------------------------------------------------------------------------- /images/MotionBlur_SceneAnimation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/MotionBlur_SceneAnimation.png -------------------------------------------------------------------------------- /images/MoveKeyframesAndMarkers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/MoveKeyframesAndMarkers.png -------------------------------------------------------------------------------- /images/SceneBuilder_Background_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/SceneBuilder_Background_v2.png -------------------------------------------------------------------------------- /images/SceneBuilder_BuildAll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/SceneBuilder_BuildAll.png -------------------------------------------------------------------------------- /images/SceneBuilder_BuildInParts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/SceneBuilder_BuildInParts.png -------------------------------------------------------------------------------- /images/SceneBuilder_FloorShadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/SceneBuilder_FloorShadow.png -------------------------------------------------------------------------------- /images/SceneBuilder_FloorShadow_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/SceneBuilder_FloorShadow_v2.png -------------------------------------------------------------------------------- /images/SceneBuilder_FuzzyFloor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/SceneBuilder_FuzzyFloor.png -------------------------------------------------------------------------------- /images/SceneBuilder_HDRI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/SceneBuilder_HDRI.png -------------------------------------------------------------------------------- /images/SceneBuilder_HDRI_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/SceneBuilder_HDRI_v2.png -------------------------------------------------------------------------------- /images/cameraControl_animate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/cameraControl_animate.png -------------------------------------------------------------------------------- /images/floorNormal_3DView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/floorNormal_3DView.png -------------------------------------------------------------------------------- /images/floorNormal_Modifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/floorNormal_Modifier.png -------------------------------------------------------------------------------- /images/floorNormal_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/floorNormal_back.png -------------------------------------------------------------------------------- /images/floorNormal_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/floorNormal_front.png -------------------------------------------------------------------------------- /images/floor_clampDodge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/floor_clampDodge.png -------------------------------------------------------------------------------- /images/fuzzytools_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/fuzzytools_banner.png -------------------------------------------------------------------------------- /images/renameVariant_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/renameVariant_after.png -------------------------------------------------------------------------------- /images/renameVariant_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagotoons/fuzzytools/6f6ac367f90009c02edd2cecf771f665b7a18c11/images/renameVariant_before.png --------------------------------------------------------------------------------