├── .gitattributes ├── .gitignore ├── README.md ├── __init__.py ├── animation ├── __init__.py ├── frame_current.py ├── jump_frames.py ├── motion_paths.py └── timeline_extra_info.py ├── misc ├── __init__.py ├── color_management.py ├── dopesheet_grapheditor.py ├── dupli_group_id.py ├── sequencer_extra_info.py └── toggle_wire.py ├── modeling ├── __init__.py └── symmetry_tools.py ├── node_editor ├── __init__.py ├── display_image.py ├── id_panel.py ├── node_shader_extra.py ├── node_stats.py ├── normal_node.py ├── simplify_nodes.py ├── switch_material.py └── templates │ ├── __init__.py │ ├── vectorblur.py │ └── vignette.py ├── prefs.py ├── render ├── __init__.py ├── border_camera.py ├── final_resolution.py ├── meshlight_add.py ├── meshlight_select.py ├── only_render.py ├── passepartout.py ├── remember_layers.py ├── render_output_z.py ├── samples_scene.py └── unsimplify.py ├── scene ├── __init__.py ├── current_blend.py ├── debug.py ├── goto_library.py ├── material_remove_unassigned.py ├── refresh.py ├── save_reload.py └── stats.py └── utils.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[cod] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | .ropeproject/ 204 | 205 | # Installer logs 206 | pip-log.txt 207 | 208 | # Unit test / coverage reports 209 | .coverage 210 | .tox 211 | 212 | #Translations 213 | *.mo 214 | 215 | #Mr Developer 216 | .mr.developer.cfg 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Amaranth 2 | = 3 | 4 | Amaranth is now a [Blender Extension](https://extensions.blender.org/add-ons/amaranth/), the code can be found on [projects.blender.org](https://projects.blender.org/extensions/amaranth) 5 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Amaranth 16 | 17 | Using Blender every day, you get to change little things on it to speedup 18 | your workflow. The problem is when you have to switch computers with 19 | somebody else's Blender, it sucks. 20 | That's the main reason behind Amaranth. I ported all sort of little changes 21 | I find useful into this addon. 22 | 23 | What is it about? Anything, whatever I think it can speedup workflow, 24 | I'll try to add it. Enjoy <3 25 | """ 26 | 27 | import sys 28 | 29 | # import amaranth's modules 30 | 31 | # NOTE: avoid local imports whenever possible! 32 | # Thanks to Christopher Crouzet for let me know about this. 33 | # http://stackoverflow.com/questions/13392038/python-making-a-class-variable-static-even-when-a-module-is-imported-in-differe 34 | 35 | from amaranth import prefs 36 | 37 | from amaranth.modeling import symmetry_tools 38 | 39 | from amaranth.scene import ( 40 | refresh, 41 | save_reload, 42 | current_blend, 43 | stats, 44 | goto_library, 45 | debug, 46 | material_remove_unassigned, 47 | ) 48 | 49 | from amaranth.node_editor import ( 50 | id_panel, 51 | display_image, 52 | templates, 53 | simplify_nodes, 54 | node_stats, 55 | normal_node, 56 | switch_material, 57 | node_shader_extra, 58 | ) 59 | 60 | from amaranth.render import ( 61 | border_camera, 62 | meshlight_add, 63 | meshlight_select, 64 | passepartout, 65 | final_resolution, 66 | samples_scene, 67 | render_output_z, 68 | ) 69 | 70 | from amaranth.animation import ( 71 | timeline_extra_info, 72 | frame_current, 73 | motion_paths, 74 | jump_frames, 75 | ) 76 | 77 | from amaranth.misc import ( 78 | dopesheet_grapheditor, 79 | color_management, 80 | dupli_group_id, 81 | toggle_wire, 82 | sequencer_extra_info, 83 | ) 84 | 85 | 86 | # register the addon + modules found in globals() 87 | bl_info = { 88 | "name": "Amaranth Toolset", 89 | "author": "Pablo Vazquez, Bassam Kurdali, Sergey Sharybin, Lukas Tönne, Cesar Saez, CansecoGPC", 90 | "version": (1, 0, 8), 91 | "blender": (2, 81, 0), 92 | "location": "Everywhere!", 93 | "description": "A collection of tools and settings to improve productivity", 94 | "warning": "", 95 | "wiki_url": "https://pablovazquez.art/amaranth", 96 | "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/", 97 | "category": "Interface", 98 | } 99 | 100 | 101 | def _call_globals(attr_name): 102 | for m in globals().values(): 103 | if hasattr(m, attr_name): 104 | getattr(m, attr_name)() 105 | 106 | 107 | def _flush_modules(pkg_name): 108 | pkg_name = pkg_name.lower() 109 | for k in tuple(sys.modules.keys()): 110 | if k.lower().startswith(pkg_name): 111 | del sys.modules[k] 112 | 113 | 114 | def register(): 115 | _call_globals("register") 116 | 117 | 118 | def unregister(): 119 | _call_globals("unregister") 120 | _flush_modules("amaranth") # reload amaranth 121 | -------------------------------------------------------------------------------- /animation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/venomgfx/amaranth/e2a34ae7fb86f66ac931a40cdc9d20d0f2ebb583/animation/__init__.py -------------------------------------------------------------------------------- /animation/frame_current.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Current Frame Slider 16 | 17 | Currently the only way to change the current frame is to have a Timeline 18 | editor open, but sometimes you don't have one, or you're fullscreen. 19 | This option adds the Current Frame slider to the Specials menu. Find it 20 | hitting the W menu in Object mode, you can slide or click in the middle 21 | of the button to set the frame manually. 22 | """ 23 | 24 | import bpy 25 | 26 | 27 | def button_frame_current(self, context): 28 | get_addon = "amaranth" in context.preferences.addons.keys() 29 | if not get_addon: 30 | return 31 | 32 | scene = context.scene 33 | if context.preferences.addons["amaranth"].preferences.use_frame_current: 34 | self.layout.separator() 35 | self.layout.prop(scene, "frame_current", text="Set Current Frame") 36 | 37 | 38 | def register(): 39 | bpy.types.VIEW3D_MT_object_context_menu.append(button_frame_current) 40 | bpy.types.VIEW3D_MT_pose_context_menu.append(button_frame_current) 41 | 42 | 43 | def unregister(): 44 | bpy.types.VIEW3D_MT_object_context_menu.remove(button_frame_current) 45 | bpy.types.VIEW3D_MT_pose_context_menu.remove(button_frame_current) 46 | -------------------------------------------------------------------------------- /animation/jump_frames.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Jump X Frames on Shift Up/Down 16 | 17 | When you hit Shift Up/Down, you'll jump 10 frames forward/backwards. 18 | Sometimes is nice to tweak that value. 19 | 20 | In the User Preferences, Editing tab, you'll find a "Frames to Jump" 21 | slider where you can adjust how many frames you'd like to move 22 | forwards/backwards. 23 | 24 | Make sure you save your user settings if you want to use this value from 25 | now on. 26 | 27 | Find it on the User Preferences, Editing. 28 | """ 29 | 30 | import bpy 31 | from bpy.types import Operator 32 | from bpy.props import BoolProperty 33 | 34 | KEYMAPS = list() 35 | 36 | 37 | # FUNCTION: Check if object has keyframes for a specific frame 38 | def is_keyframe(ob, frame): 39 | if ob is not None and ob.animation_data is not None and ob.animation_data.action is not None: 40 | for fcu in ob.animation_data.action.fcurves: 41 | if frame in (p.co.x for p in fcu.keyframe_points): 42 | return True 43 | return False 44 | 45 | 46 | # monkey path is_keyframe function 47 | bpy.types.Object.is_keyframe = is_keyframe 48 | 49 | 50 | # FEATURE: Jump to frame in-between next and previous keyframe 51 | class AMTH_SCREEN_OT_keyframe_jump_inbetween(Operator): 52 | """Jump to half in-between keyframes""" 53 | bl_idname = "screen.amth_keyframe_jump_inbetween" 54 | bl_label = "Jump to Keyframe In-between" 55 | 56 | backwards: BoolProperty() 57 | 58 | def execute(self, context): 59 | back = self.backwards 60 | 61 | scene = context.scene 62 | ob = bpy.context.object 63 | frame_start = scene.frame_start 64 | frame_end = scene.frame_end 65 | 66 | if not context.scene.get("amth_keyframes_jump"): 67 | context.scene["amth_keyframes_jump"] = list() 68 | 69 | keyframes_list = context.scene["amth_keyframes_jump"] 70 | 71 | for f in range(frame_start, frame_end): 72 | if ob.is_keyframe(f): 73 | keyframes_list = list(keyframes_list) 74 | keyframes_list.append(f) 75 | 76 | if keyframes_list: 77 | keyframes_list_half = [] 78 | 79 | for i, item in enumerate(keyframes_list): 80 | try: 81 | next_item = keyframes_list[i + 1] 82 | keyframes_list_half.append(int((item + next_item) / 2)) 83 | except: 84 | pass 85 | 86 | if len(keyframes_list_half) > 1: 87 | if back: 88 | v = (scene.frame_current == keyframes_list_half[::-1][-1], 89 | scene.frame_current < keyframes_list_half[::-1][-1]) 90 | if any(v): 91 | self.report({"INFO"}, "No keyframes behind") 92 | else: 93 | for i in keyframes_list_half[::-1]: 94 | if scene.frame_current > i: 95 | scene.frame_current = i 96 | break 97 | else: 98 | v = (scene.frame_current == keyframes_list_half[-1], 99 | scene.frame_current > keyframes_list_half[-1]) 100 | if any(v): 101 | self.report({"INFO"}, "No keyframes ahead") 102 | else: 103 | for i in keyframes_list_half: 104 | if scene.frame_current < i: 105 | scene.frame_current = i 106 | break 107 | else: 108 | self.report({"INFO"}, "Object has only 1 keyframe") 109 | else: 110 | self.report({"INFO"}, "Object has no keyframes") 111 | 112 | return {"FINISHED"} 113 | 114 | 115 | # FEATURE: Jump forward/backward every N frames 116 | class AMTH_SCREEN_OT_frame_jump(Operator): 117 | """Jump a number of frames forward/backwards""" 118 | bl_idname = "screen.amaranth_frame_jump" 119 | bl_label = "Jump Frames" 120 | 121 | forward: BoolProperty(default=True) 122 | 123 | def execute(self, context): 124 | scene = context.scene 125 | 126 | get_addon = "amaranth" in context.preferences.addons.keys() 127 | if not get_addon: 128 | return {"CANCELLED"} 129 | 130 | preferences = context.preferences.addons["amaranth"].preferences 131 | 132 | if preferences.use_framerate: 133 | framedelta = scene.render.fps 134 | else: 135 | framedelta = preferences.frames_jump 136 | if self.forward: 137 | scene.frame_current = scene.frame_current + framedelta 138 | else: 139 | scene.frame_current = scene.frame_current - framedelta 140 | 141 | return {"FINISHED"} 142 | 143 | 144 | def ui_userpreferences_edit(self, context): 145 | get_addon = "amaranth" in context.preferences.addons.keys() 146 | if not get_addon: 147 | return 148 | 149 | preferences = context.preferences.addons["amaranth"].preferences 150 | 151 | col = self.layout.column() 152 | split = col.split(factor=0.21) 153 | split.prop(preferences, "frames_jump", 154 | text="Frames to Jump") 155 | 156 | 157 | def label(self, context): 158 | get_addon = "amaranth" in context.preferences.addons.keys() 159 | if not get_addon: 160 | return 161 | 162 | layout = self.layout 163 | 164 | if context.preferences.addons["amaranth"].preferences.use_timeline_extra_info: 165 | row = layout.row(align=True) 166 | 167 | row.operator(AMTH_SCREEN_OT_keyframe_jump_inbetween.bl_idname, 168 | icon="PREV_KEYFRAME", text="").backwards = True 169 | row.operator(AMTH_SCREEN_OT_keyframe_jump_inbetween.bl_idname, 170 | icon="NEXT_KEYFRAME", text="").backwards = False 171 | 172 | 173 | def register(): 174 | bpy.utils.register_class(AMTH_SCREEN_OT_frame_jump) 175 | bpy.utils.register_class(AMTH_SCREEN_OT_keyframe_jump_inbetween) 176 | bpy.types.USERPREF_PT_animation_timeline.append(ui_userpreferences_edit) 177 | bpy.types.USERPREF_PT_animation_timeline.append(label) 178 | 179 | # register keyboard shortcuts 180 | wm = bpy.context.window_manager 181 | kc = wm.keyconfigs.addon 182 | 183 | km = kc.keymaps.new(name="Frames") 184 | kmi = km.keymap_items.new('screen.amth_keyframe_jump_inbetween', 'UP_ARROW', 'PRESS', shift=True, ctrl=True) 185 | kmi.properties.backwards = False 186 | KEYMAPS.append((km, kmi)) 187 | 188 | kmi = km.keymap_items.new('screen.amth_keyframe_jump_inbetween', 'DOWN_ARROW', 'PRESS', shift=True, ctrl=True) 189 | kmi.properties.backwards = True 190 | KEYMAPS.append((km, kmi)) 191 | 192 | kmi = km.keymap_items.new( 193 | "screen.amaranth_frame_jump", "UP_ARROW", "PRESS", shift=True) 194 | kmi.properties.forward = True 195 | KEYMAPS.append((km, kmi)) 196 | 197 | kmi = km.keymap_items.new( 198 | "screen.amaranth_frame_jump", "DOWN_ARROW", "PRESS", shift=True) 199 | kmi.properties.forward = False 200 | KEYMAPS.append((km, kmi)) 201 | 202 | 203 | def unregister(): 204 | bpy.utils.unregister_class(AMTH_SCREEN_OT_frame_jump) 205 | bpy.utils.unregister_class(AMTH_SCREEN_OT_keyframe_jump_inbetween) 206 | bpy.types.USERPREF_PT_animation_timeline.remove(ui_userpreferences_edit) 207 | for km, kmi in KEYMAPS: 208 | km.keymap_items.remove(kmi) 209 | KEYMAPS.clear() 210 | -------------------------------------------------------------------------------- /animation/motion_paths.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Bone Motion Paths: 16 | 17 | Match Frame Range + Clear All Paths 18 | 19 | * Clear All Paths: 20 | Silly operator to loop through all bones and clear their paths, useful 21 | when having hidden bones (othrewise you have to go through each one of 22 | them and clear manually) 23 | 24 | *Match Current Frame Range: 25 | Set the current frame range as motion path range. 26 | 27 | Both requests by Hjalti from Project Pampa 28 | Thanks to Bassam Kurdali for helping finding out the weirdness behind 29 | Motion Paths bpy. 30 | 31 | Developed during Caminandes Open Movie Project 32 | """ 33 | 34 | import bpy 35 | 36 | 37 | class AMTH_POSE_OT_paths_clear_all(bpy.types.Operator): 38 | 39 | """Clear motion paths from all bones""" 40 | bl_idname = "pose.paths_clear_all" 41 | bl_label = "Clear All Motion Paths" 42 | bl_options = {"UNDO"} 43 | 44 | @classmethod 45 | def poll(cls, context): 46 | return context.mode == "POSE" 47 | 48 | def execute(self, context): 49 | # silly but works 50 | for b in context.object.data.bones: 51 | b.select = True 52 | bpy.ops.pose.paths_clear() 53 | b.select = False 54 | return {"FINISHED"} 55 | 56 | 57 | class AMTH_POSE_OT_paths_frame_match(bpy.types.Operator): 58 | 59 | """Match Start/End frame of scene to motion path range""" 60 | bl_idname = "pose.paths_frame_match" 61 | bl_label = "Match Frame Range" 62 | bl_options = {"UNDO"} 63 | 64 | def execute(self, context): 65 | avs = context.object.pose.animation_visualization 66 | scene = context.scene 67 | 68 | if avs.motion_path.type == "RANGE": 69 | if scene.use_preview_range: 70 | avs.motion_path.frame_start = scene.frame_preview_start 71 | avs.motion_path.frame_end = scene.frame_preview_end 72 | else: 73 | avs.motion_path.frame_start = scene.frame_start 74 | avs.motion_path.frame_end = scene.frame_end 75 | 76 | else: 77 | if scene.use_preview_range: 78 | avs.motion_path.frame_before = scene.frame_preview_start 79 | avs.motion_path.frame_after = scene.frame_preview_end 80 | else: 81 | avs.motion_path.frame_before = scene.frame_start 82 | avs.motion_path.frame_after = scene.frame_end 83 | 84 | return {"FINISHED"} 85 | 86 | 87 | def pose_motion_paths_ui(self, context): 88 | 89 | layout = self.layout 90 | scene = context.scene 91 | avs = context.object.pose.animation_visualization 92 | if context.active_pose_bone: 93 | mpath = context.active_pose_bone.motion_path 94 | layout.separator() 95 | layout.label(text="Motion Paths Extras:") 96 | 97 | split = layout.split() 98 | 99 | col = split.column(align=True) 100 | 101 | if context.selected_pose_bones: 102 | if mpath: 103 | sub = col.row(align=True) 104 | sub.operator( 105 | "pose.paths_update", text="Update Path", icon="BONE_DATA") 106 | sub.operator("pose.paths_clear", text="", icon="X") 107 | else: 108 | col.operator( 109 | "pose.paths_calculate", 110 | text="Calculate Path", 111 | icon="BONE_DATA") 112 | else: 113 | col.label(text="Select Bones First", icon="ERROR") 114 | 115 | col = split.column(align=True) 116 | col.operator( 117 | AMTH_POSE_OT_paths_frame_match.bl_idname, 118 | text="Set Preview Frame Range" if scene.use_preview_range else "Set Frame Range", 119 | icon="PREVIEW_RANGE" if scene.use_preview_range else "TIME") 120 | 121 | col = layout.column() 122 | row = col.row(align=True) 123 | 124 | if avs.motion_path.type == "RANGE": 125 | row.prop(avs.motion_path, "frame_start", text="Start") 126 | row.prop(avs.motion_path, "frame_end", text="End") 127 | else: 128 | row.prop(avs.motion_path, "frame_before", text="Before") 129 | row.prop(avs.motion_path, "frame_after", text="After") 130 | 131 | layout.separator() 132 | layout.operator(AMTH_POSE_OT_paths_clear_all.bl_idname, icon="X") 133 | 134 | 135 | def register(): 136 | bpy.utils.register_class(AMTH_POSE_OT_paths_clear_all) 137 | bpy.utils.register_class(AMTH_POSE_OT_paths_frame_match) 138 | bpy.types.DATA_PT_display.append(pose_motion_paths_ui) 139 | 140 | 141 | def unregister(): 142 | bpy.utils.unregister_class(AMTH_POSE_OT_paths_clear_all) 143 | bpy.utils.unregister_class(AMTH_POSE_OT_paths_frame_match) 144 | bpy.types.DATA_PT_display.remove(pose_motion_paths_ui) 145 | -------------------------------------------------------------------------------- /animation/timeline_extra_info.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Timeline Extra Info 16 | 17 | Display amount of frames left until Frame End, very handy especially when 18 | rendering an animation or OpenGL preview. 19 | Display current/end time on SMPTE. Find it on the Timeline header. 20 | """ 21 | 22 | import bpy 23 | 24 | 25 | def label_timeline_extra_info(self, context): 26 | get_addon = "amaranth" in context.preferences.addons.keys() 27 | if not get_addon: 28 | return 29 | 30 | layout = self.layout 31 | scene = context.scene 32 | 33 | if context.preferences.addons["amaranth"].preferences.use_timeline_extra_info: 34 | row = layout.row(align=True) 35 | 36 | # Check for preview range 37 | frame_start = scene.frame_preview_start if scene.use_preview_range else scene.frame_start 38 | frame_end = scene.frame_preview_end if scene.use_preview_range else scene.frame_end 39 | 40 | row.label( 41 | text="%s / %s" % 42 | (bpy.utils.smpte_from_frame( 43 | scene.frame_current - 44 | frame_start), 45 | bpy.utils.smpte_from_frame( 46 | frame_end - 47 | frame_start))) 48 | 49 | if (scene.frame_current > frame_end): 50 | row.label(text="%s Frames Ahead" % 51 | ((frame_end - scene.frame_current) * -1)) 52 | elif (scene.frame_current == frame_start): 53 | row.label(text="Start Frame (%s left)" % 54 | (frame_end - scene.frame_current)) 55 | elif (scene.frame_current == frame_end): 56 | row.label(text="%s End Frame" % scene.frame_current) 57 | else: 58 | row.label(text="%s Frames Left" % 59 | (frame_end - scene.frame_current)) 60 | 61 | 62 | def register(): 63 | bpy.types.STATUSBAR_HT_header.append(label_timeline_extra_info) 64 | 65 | 66 | def unregister(): 67 | bpy.types.STATUSBAR_HT_header.remove(label_timeline_extra_info) 68 | -------------------------------------------------------------------------------- /misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/venomgfx/amaranth/e2a34ae7fb86f66ac931a40cdc9d20d0f2ebb583/misc/__init__.py -------------------------------------------------------------------------------- /misc/color_management.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Color Management Presets 16 | 17 | Save your Color Management options as presets, for easy re-use. 18 | 19 | It will pretty much every option in the Color Management panel, such as 20 | the look, color settings, and so on. Except the curve points (have to 21 | figure out how to do that nicely), good news is that in Blender 2.69+ you 22 | can now copy/paste curves. 23 | """ 24 | 25 | import bpy 26 | from bl_operators.presets import AddPresetBase 27 | 28 | 29 | class AMTH_SCENE_MT_color_management_presets(bpy.types.Menu): 30 | 31 | """List of Color Management presets""" 32 | bl_label = "Color Management Presets" 33 | preset_subdir = "color" 34 | preset_operator = "script.execute_preset" 35 | draw = bpy.types.Menu.draw_preset 36 | 37 | 38 | class AMTH_AddPresetColorManagement(AddPresetBase, bpy.types.Operator): 39 | 40 | """Add or remove a Color Management preset""" 41 | bl_idname = "scene.color_management_preset_add" 42 | bl_label = "Add Color Management Preset" 43 | preset_menu = "AMTH_SCENE_MT_color_management_presets" 44 | 45 | preset_defines = [ 46 | "scene = bpy.context.scene", 47 | ] 48 | 49 | preset_values = [ 50 | "scene.view_settings.view_transform", 51 | "scene.display_settings.display_device", 52 | "scene.view_settings.exposure", 53 | "scene.view_settings.gamma", 54 | "scene.view_settings.look", 55 | "scene.view_settings.use_curve_mapping", 56 | "scene.sequencer_colorspace_settings.name", 57 | ] 58 | 59 | preset_subdir = "color" 60 | 61 | 62 | def ui_color_management_presets(self, context): 63 | 64 | layout = self.layout 65 | 66 | row = layout.row(align=True) 67 | row.menu("AMTH_SCENE_MT_color_management_presets", 68 | text=bpy.types.AMTH_SCENE_MT_color_management_presets.bl_label) 69 | row.operator("scene.color_management_preset_add", text="", icon="ZOOM_IN") 70 | row.operator("scene.color_management_preset_add", 71 | text="", icon="ZOOM_OUT").remove_active = True 72 | layout.separator() 73 | 74 | 75 | def register(): 76 | bpy.utils.register_class(AMTH_AddPresetColorManagement) 77 | bpy.utils.register_class(AMTH_SCENE_MT_color_management_presets) 78 | bpy.types.RENDER_PT_color_management.prepend(ui_color_management_presets) 79 | 80 | 81 | def unregister(): 82 | bpy.utils.unregister_class(AMTH_AddPresetColorManagement) 83 | bpy.utils.unregister_class(AMTH_SCENE_MT_color_management_presets) 84 | bpy.types.RENDER_PT_color_management.remove(ui_color_management_presets) 85 | -------------------------------------------------------------------------------- /misc/dopesheet_grapheditor.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Shortcut: Ctrl+Tab to switch between Dopesheet/Graph Editor 16 | 17 | Hit Ctrl + Tab to switch between Dopesheet and the Graph Editor. 18 | Developed during Caminandes Open Movie Project 19 | """ 20 | 21 | 22 | import bpy 23 | 24 | KEYMAPS = list() 25 | 26 | 27 | def register(): 28 | wm = bpy.context.window_manager 29 | kc = wm.keyconfigs.addon 30 | 31 | km = kc.keymaps.new(name="Graph Editor", space_type="GRAPH_EDITOR") 32 | kmi = km.keymap_items.new("wm.context_set_enum", "TAB", "PRESS", ctrl=True) 33 | kmi.properties.data_path = "area.type" 34 | kmi.properties.value = "DOPESHEET_EDITOR" 35 | KEYMAPS.append((km, kmi)) 36 | 37 | km = kc.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") 38 | kmi = km.keymap_items.new("wm.context_set_enum", "TAB", "PRESS", ctrl=True) 39 | kmi.properties.data_path = "area.type" 40 | kmi.properties.value = "GRAPH_EDITOR" 41 | KEYMAPS.append((km, kmi)) 42 | 43 | km = kc.keymaps.new(name="Dopesheet", space_type="DOPESHEET_EDITOR") 44 | kmi = km.keymap_items.new("wm.context_toggle_enum", 45 | "TAB", "PRESS", shift=True) 46 | kmi.properties.data_path = "space_data.mode" 47 | kmi.properties.value_1 = "ACTION" 48 | kmi.properties.value_2 = "DOPESHEET" 49 | KEYMAPS.append((km, kmi)) 50 | 51 | 52 | def unregister(): 53 | for km, kmi in KEYMAPS: 54 | km.keymap_items.remove(kmi) 55 | KEYMAPS.clear() 56 | -------------------------------------------------------------------------------- /misc/dupli_group_id.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Object ID for Dupli Groups 16 | Say you have a linked character or asset, you can now set an Object ID for the 17 | entire instance (the objects in the group), and use it with the Object Index 18 | pass later in compositing. Something that I always wanted and it wasn't 19 | possible! 20 | 21 | In order for the Object ID to be loaded afterwards on computers without 22 | Amaranth installed, it will automatically create a text file (called 23 | AmaranthStartup.py) and save it inside the .blend, this will autorun on 24 | startup and set the OB IDs. Remember to have auto-run python scripts on your 25 | startup preferences. 26 | 27 | Set a Pass Index and press "Apply Object ID to Duplis" on the Relations panel, 28 | Object Properties. 29 | """ 30 | 31 | 32 | import bpy 33 | from amaranth.scene.debug import AMTH_SCENE_OT_blender_instance_open 34 | 35 | 36 | # Some settings are bound to be saved on a startup py file 37 | # TODO: refactor this, amth_text should not be declared as a global variable, 38 | # otherwise becomes confusing when you call it in the classes below. 39 | def amaranth_text_startup(context): 40 | 41 | amth_text_name = "AmaranthStartup.py" 42 | amth_text_exists = False 43 | 44 | global amth_text 45 | 46 | try: 47 | if bpy.data.texts: 48 | for tx in bpy.data.texts: 49 | if tx.name == amth_text_name: 50 | amth_text_exists = True 51 | amth_text = bpy.data.texts[amth_text_name] 52 | break 53 | else: 54 | amth_text_exists = False 55 | 56 | if not amth_text_exists: 57 | bpy.ops.text.new() 58 | amth_text = bpy.data.texts[((len(bpy.data.texts) * -1) + 1)] 59 | amth_text.name = amth_text_name 60 | amth_text.write("# Amaranth Startup Script\nimport bpy\n") 61 | amth_text.use_module = True 62 | 63 | return amth_text_exists 64 | except AttributeError: 65 | return None 66 | 67 | 68 | # FEATURE: Dupli Group Path 69 | def ui_dupli_group_library_path(self, context): 70 | 71 | ob = context.object 72 | 73 | row = self.layout.row() 74 | row.alignment = "LEFT" 75 | 76 | if ob and ob.instance_collection and ob.instance_collection.library: 77 | lib = ob.instance_collection.library.filepath 78 | 79 | row.operator(AMTH_SCENE_OT_blender_instance_open.bl_idname, 80 | text="Library: %s" % lib, 81 | emboss=False, 82 | icon="LINK_BLEND").filepath = lib 83 | # // FEATURE: Dupli Group Path 84 | 85 | 86 | # FEATURE: Object ID for objects inside DupliGroups 87 | class AMTH_OBJECT_OT_id_dupligroup(bpy.types.Operator): 88 | 89 | """Set the Object ID for objects in the dupli group""" 90 | bl_idname = "object.amaranth_object_id_duplis" 91 | bl_label = "Apply Object ID to Duplis" 92 | 93 | clear = False 94 | 95 | @classmethod 96 | def poll(cls, context): 97 | return context.active_object.instance_collection 98 | 99 | def execute(self, context): 100 | self.__class__.clear = False 101 | ob = context.active_object 102 | amaranth_text_startup(context) 103 | script_exists = False 104 | script_intro = "# OB ID: %s" % ob.name 105 | obdata = 'bpy.data.objects[" % s"]' % ob.name 106 | # TODO: cleanup script var using format or template strings 107 | script = "%s" % ( 108 | "\nif %(obdata)s and %(obdata)s.instance_collection and %(obdata)s.pass_index != 0: %(obname)s \n" 109 | " for dob in %(obdata)s.instance_collection.objects: %(obname)s \n" 110 | " dob.pass_index = %(obdata)s.pass_index %(obname)s \n" % 111 | {"obdata": obdata, "obname": script_intro}) 112 | 113 | for txt in bpy.data.texts: 114 | if txt.name == amth_text.name: 115 | for li in txt.lines: 116 | if script_intro == li.body: 117 | script_exists = True 118 | continue 119 | 120 | if not script_exists: 121 | amth_text.write("\n") 122 | amth_text.write(script_intro) 123 | amth_text.write(script) 124 | 125 | if ob and ob.instance_collection: 126 | if ob.pass_index != 0: 127 | for dob in ob.instance_collection.objects: 128 | dob.pass_index = ob.pass_index 129 | 130 | self.report({"INFO"}, 131 | "%s ID: %s to all objects in this Dupli Group" % ( 132 | "Applied" if not script_exists else "Updated", 133 | ob.pass_index)) 134 | 135 | return {"FINISHED"} 136 | 137 | 138 | class AMTH_OBJECT_OT_id_dupligroup_clear(bpy.types.Operator): 139 | 140 | """Clear the Object ID from objects in dupli group""" 141 | bl_idname = "object.amaranth_object_id_duplis_clear" 142 | bl_label = "Clear Object ID from Duplis" 143 | 144 | @classmethod 145 | def poll(cls, context): 146 | return context.active_object.instance_collection 147 | 148 | def execute(self, context): 149 | context.active_object.pass_index = 0 150 | AMTH_OBJECT_OT_id_dupligroup.clear = True 151 | amth_text_exists = amaranth_text_startup(context) 152 | match_first = "# OB ID: %s" % context.active_object.name 153 | 154 | if amth_text_exists: 155 | for txt in bpy.data.texts: 156 | if txt.name == amth_text.name: 157 | for li in txt.lines: 158 | if match_first in li.body: 159 | li.body = "" 160 | continue 161 | 162 | self.report({"INFO"}, "Object IDs back to normal") 163 | return {"FINISHED"} 164 | 165 | 166 | def ui_object_id_duplis(self, context): 167 | 168 | if context.active_object.instance_collection: 169 | split = self.layout.split() 170 | row = split.row(align=True) 171 | row.enabled = context.active_object.pass_index != 0 172 | row.operator( 173 | AMTH_OBJECT_OT_id_dupligroup.bl_idname) 174 | row.operator( 175 | AMTH_OBJECT_OT_id_dupligroup_clear.bl_idname, 176 | icon="X", text="") 177 | split.separator() 178 | 179 | if AMTH_OBJECT_OT_id_dupligroup.clear: 180 | self.layout.label(text="Next time you save/reload this file, " 181 | "object IDs will be back to normal", 182 | icon="INFO") 183 | # // FEATURE: Object ID for objects inside DupliGroups 184 | 185 | 186 | def register(): 187 | bpy.utils.register_class(AMTH_OBJECT_OT_id_dupligroup) 188 | bpy.utils.register_class(AMTH_OBJECT_OT_id_dupligroup_clear) 189 | bpy.types.OBJECT_PT_instancing.append(ui_dupli_group_library_path) 190 | bpy.types.OBJECT_PT_relations.append(ui_object_id_duplis) 191 | 192 | 193 | def unregister(): 194 | bpy.utils.unregister_class(AMTH_OBJECT_OT_id_dupligroup) 195 | bpy.utils.unregister_class(AMTH_OBJECT_OT_id_dupligroup_clear) 196 | bpy.types.OBJECT_PT_instancing.remove(ui_dupli_group_library_path) 197 | bpy.types.OBJECT_PT_relations.remove(ui_object_id_duplis) 198 | -------------------------------------------------------------------------------- /misc/sequencer_extra_info.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | # This program is free software; you can redistribute it and/or 15 | # modify it under the terms of the GNU General Public License 16 | # as published by the Free Software Foundation; either version 2 17 | # of the License, or (at your option) any later version. 18 | # 19 | # This program is distributed in the hope that it will be useful, 20 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | # GNU General Public License for more details. 23 | # 24 | # You should have received a copy of the GNU General Public License 25 | # along with this program; if not, write to the Free Software Foundation, 26 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 27 | """ 28 | Sequencer: Display Image File Name 29 | 30 | When seeking through an image sequence, display the active strips' file name 31 | for the current frame, and it's [playhead]. 32 | 33 | Find it on the VSE header. 34 | """ 35 | import bpy 36 | 37 | 38 | # FEATURE: Sequencer Extra Info 39 | def act_strip(context): 40 | try: 41 | return context.scene.sequence_editor.active_strip 42 | except AttributeError: 43 | return None 44 | 45 | 46 | def ui_sequencer_extra_info(self, context): 47 | layout = self.layout 48 | strip = act_strip(context) 49 | if strip: 50 | seq_type = strip.type 51 | if seq_type and seq_type == 'IMAGE': 52 | elem = strip.strip_elem_from_frame(context.scene.frame_current) 53 | if elem: 54 | layout.label( 55 | text="%s %s" % 56 | (elem.filename, "[%s]" % 57 | (context.scene.frame_current - strip.frame_start))) 58 | 59 | # // FEATURE: Sequencer Extra Info 60 | 61 | 62 | def register(): 63 | bpy.types.SEQUENCER_HT_header.append(ui_sequencer_extra_info) 64 | 65 | 66 | def unregister(): 67 | bpy.types.SEQUENCER_HT_header.remove(ui_sequencer_extra_info) 68 | -------------------------------------------------------------------------------- /misc/toggle_wire.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | 15 | import bpy 16 | 17 | 18 | # FEATURE: Toggle Wire Display 19 | class AMTH_OBJECT_OT_wire_toggle(bpy.types.Operator): 20 | 21 | """Turn on/off wire display on mesh objects""" 22 | bl_idname = "object.amth_wire_toggle" 23 | bl_label = "Display Wireframe" 24 | bl_options = {"REGISTER", "UNDO"} 25 | 26 | clear: bpy.props.BoolProperty( 27 | default=False, name="Clear Wireframe", 28 | description="Clear Wireframe Display") 29 | 30 | def execute(self, context): 31 | 32 | scene = context.scene 33 | is_all_scenes = scene.amth_wire_toggle_scene_all 34 | is_selected = scene.amth_wire_toggle_is_selected 35 | is_all_edges = scene.amth_wire_toggle_edges_all 36 | is_optimal = scene.amth_wire_toggle_optimal 37 | clear = self.clear 38 | 39 | if is_all_scenes: 40 | which = bpy.data.objects 41 | elif is_selected: 42 | if not context.selected_objects: 43 | self.report({"INFO"}, "No selected objects") 44 | which = context.selected_objects 45 | else: 46 | which = scene.objects 47 | 48 | if which: 49 | for ob in which: 50 | if ob and ob.type in { 51 | "MESH", "EMPTY", "CURVE", 52 | "META", "SURFACE", "FONT"}: 53 | 54 | ob.show_wire = False if clear else True 55 | ob.show_all_edges = is_all_edges 56 | 57 | for mo in ob.modifiers: 58 | if mo and mo.type == "SUBSURF": 59 | mo.show_only_control_edges = is_optimal 60 | 61 | return {"FINISHED"} 62 | 63 | 64 | def ui_object_wire_toggle(self, context): 65 | 66 | scene = context.scene 67 | 68 | self.layout.separator() 69 | col = self.layout.column(align=True) 70 | col.label(text="Wireframes:") 71 | row = col.row(align=True) 72 | row.operator(AMTH_OBJECT_OT_wire_toggle.bl_idname, 73 | icon="MOD_WIREFRAME", text="Display").clear = False 74 | row.operator(AMTH_OBJECT_OT_wire_toggle.bl_idname, 75 | icon="X", text="Clear").clear = True 76 | col.separator() 77 | row = col.row(align=True) 78 | row.prop(scene, "amth_wire_toggle_edges_all") 79 | row.prop(scene, "amth_wire_toggle_optimal") 80 | row = col.row(align=True) 81 | sub = row.row(align=True) 82 | sub.active = not scene.amth_wire_toggle_scene_all 83 | sub.prop(scene, "amth_wire_toggle_is_selected") 84 | sub = row.row(align=True) 85 | sub.active = not scene.amth_wire_toggle_is_selected 86 | sub.prop(scene, "amth_wire_toggle_scene_all") 87 | 88 | 89 | def init_properties(): 90 | scene = bpy.types.Scene 91 | scene.amth_wire_toggle_scene_all = bpy.props.BoolProperty( 92 | default=False, 93 | name="All Scenes", 94 | description="Toggle wire on objects in all scenes") 95 | scene.amth_wire_toggle_is_selected = bpy.props.BoolProperty( 96 | default=False, 97 | name="Only Selected Objects", 98 | description="Only toggle wire on selected objects") 99 | scene.amth_wire_toggle_edges_all = bpy.props.BoolProperty( 100 | default=True, 101 | name="Draw All Edges", 102 | description="Draw all the edges even on coplanar faces") 103 | scene.amth_wire_toggle_optimal = bpy.props.BoolProperty( 104 | default=False, 105 | name="Subsurf Optimal Display", 106 | description="Skip drawing/rendering of interior subdivided edges " 107 | "on meshes with Subdivision Surface modifier") 108 | 109 | 110 | def clear_properties(): 111 | props = ( 112 | 'amth_wire_toggle_is_selected', 113 | 'amth_wire_toggle_scene_all', 114 | "amth_wire_toggle_edges_all", 115 | "amth_wire_toggle_optimal" 116 | ) 117 | wm = bpy.context.window_manager 118 | for p in props: 119 | if p in wm: 120 | del wm[p] 121 | 122 | # //FEATURE: Toggle Wire Display 123 | 124 | 125 | def register(): 126 | init_properties() 127 | bpy.utils.register_class(AMTH_OBJECT_OT_wire_toggle) 128 | bpy.types.VIEW3D_PT_view3d_properties.append(ui_object_wire_toggle) 129 | 130 | 131 | def unregister(): 132 | bpy.utils.unregister_class(AMTH_OBJECT_OT_wire_toggle) 133 | bpy.types.VIEW3D_PT_view3d_properties.remove(ui_object_wire_toggle) 134 | clear_properties() 135 | -------------------------------------------------------------------------------- /modeling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/venomgfx/amaranth/e2a34ae7fb86f66ac931a40cdc9d20d0f2ebb583/modeling/__init__.py -------------------------------------------------------------------------------- /modeling/symmetry_tools.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Symmetry Tools: Find Asymmetric + Make Symmetric (by Sergey Sharybin) 16 | 17 | Our character wasn’t completely symmetric in some parts where it was 18 | supposed to, this could be by moving vertices by mistake or just reasons. 19 | To fix this in a fast way, Sergey coded this two super useful tools: 20 | 21 | * Find Asymmetric: 22 | Selects vertices that don’t have the same position on the opposite side. 23 | 24 | * Make Symmetric: 25 | Move selected vertices to match the position of those on the other side. 26 | 27 | This tools may not apply on every single model out there, but I tried it 28 | in many different characters and it worked. So probably better use it on 29 | those models that were already symmetric at some point, modeled with a 30 | mirror modifier or so. 31 | Search (spacebar) for "Find Asymmetric", and "Make Symmetric""Settings". 32 | 33 | > Developed during Caminandes Open Movie Project 34 | """ 35 | 36 | import bpy 37 | import bmesh 38 | from mathutils import Vector 39 | 40 | 41 | class AMTH_MESH_OT_find_asymmetric(bpy.types.Operator): 42 | 43 | """ 44 | Find asymmetric vertices 45 | """ 46 | 47 | bl_idname = "mesh.find_asymmetric" 48 | bl_label = "Find Asymmetric" 49 | bl_options = {"UNDO", "REGISTER"} 50 | 51 | @classmethod 52 | def poll(cls, context): 53 | object = context.object 54 | if object: 55 | return object.mode == "EDIT" and object.type == "MESH" 56 | return False 57 | 58 | def execute(self, context): 59 | threshold = 1e-6 60 | 61 | object = context.object 62 | bm = bmesh.from_edit_mesh(object.data) 63 | 64 | # Deselect all the vertices 65 | for v in bm.verts: 66 | v.select = False 67 | 68 | for v1 in bm.verts: 69 | if abs(v1.co[0]) < threshold: 70 | continue 71 | 72 | mirror_found = False 73 | for v2 in bm.verts: 74 | if v1 == v2: 75 | continue 76 | if v1.co[0] * v2.co[0] > 0.0: 77 | continue 78 | 79 | mirror_coord = Vector(v2.co) 80 | mirror_coord[0] *= -1 81 | if (mirror_coord - v1.co).length_squared < threshold: 82 | mirror_found = True 83 | break 84 | if not mirror_found: 85 | v1.select = True 86 | 87 | bm.select_flush_mode() 88 | 89 | bmesh.update_edit_mesh(object.data) 90 | 91 | return {"FINISHED"} 92 | 93 | 94 | class AMTH_MESH_OT_make_symmetric(bpy.types.Operator): 95 | 96 | """ 97 | Make symmetric 98 | """ 99 | 100 | bl_idname = "mesh.make_symmetric" 101 | bl_label = "Make Symmetric" 102 | bl_options = {"UNDO", "REGISTER"} 103 | 104 | @classmethod 105 | def poll(cls, context): 106 | object = context.object 107 | if object: 108 | return object.mode == "EDIT" and object.type == "MESH" 109 | return False 110 | 111 | def execute(self, context): 112 | threshold = 1e-6 113 | 114 | object = context.object 115 | bm = bmesh.from_edit_mesh(object.data) 116 | 117 | for v1 in bm.verts: 118 | if v1.co[0] < threshold: 119 | continue 120 | if not v1.select: 121 | continue 122 | 123 | closest_vert = None 124 | closest_distance = -1 125 | for v2 in bm.verts: 126 | if v1 == v2: 127 | continue 128 | if v2.co[0] > threshold: 129 | continue 130 | if not v2.select: 131 | continue 132 | 133 | mirror_coord = Vector(v2.co) 134 | mirror_coord[0] *= -1 135 | distance = (mirror_coord - v1.co).length_squared 136 | if closest_vert is None or distance < closest_distance: 137 | closest_distance = distance 138 | closest_vert = v2 139 | 140 | if closest_vert: 141 | closest_vert.select = False 142 | closest_vert.co = Vector(v1.co) 143 | closest_vert.co[0] *= -1 144 | v1.select = False 145 | 146 | for v1 in bm.verts: 147 | if v1.select: 148 | closest_vert = None 149 | closest_distance = -1 150 | for v2 in bm.verts: 151 | if v1 != v2: 152 | mirror_coord = Vector(v2.co) 153 | mirror_coord[0] *= -1 154 | distance = (mirror_coord - v1.co).length_squared 155 | if closest_vert is None or distance < closest_distance: 156 | closest_distance = distance 157 | closest_vert = v2 158 | if closest_vert: 159 | v1.select = False 160 | v1.co = Vector(closest_vert.co) 161 | v1.co[0] *= -1 162 | 163 | bm.select_flush_mode() 164 | bmesh.update_edit_mesh(object.data) 165 | 166 | return {"FINISHED"} 167 | 168 | 169 | def register(): 170 | bpy.utils.register_class(AMTH_MESH_OT_find_asymmetric) 171 | bpy.utils.register_class(AMTH_MESH_OT_make_symmetric) 172 | 173 | 174 | def unregister(): 175 | bpy.utils.unregister_class(AMTH_MESH_OT_find_asymmetric) 176 | bpy.utils.unregister_class(AMTH_MESH_OT_make_symmetric) 177 | -------------------------------------------------------------------------------- /node_editor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/venomgfx/amaranth/e2a34ae7fb86f66ac931a40cdc9d20d0f2ebb583/node_editor/__init__.py -------------------------------------------------------------------------------- /node_editor/display_image.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Display Active Image Node on Image Editor 16 | 17 | When selecting an Image node, it will show it on the Image editor (if 18 | there is any available). If you don't like this behavior, you can 19 | disable it from the Amaranth Toolset panel on the Scene properties. 20 | Coded by the awesome Sergey Sharybin. This feature only works on Blender 21 | 2.68 and newer. Select an Image Node in the Compositor or Cycles nodes 22 | editor, there must be at least one image editor available. 23 | """ 24 | 25 | import bpy 26 | 27 | 28 | KEYMAPS = list() 29 | 30 | image_nodes = ("CompositorNodeRLayers", 31 | "CompositorNodeImage", 32 | "CompositorNodeViewer", 33 | "CompositorNodeComposite", 34 | "ShaderNodeTexImage", 35 | "ShaderNodeTexEnvironment") 36 | 37 | 38 | class AMTH_NODE_OT_show_active_node_image(bpy.types.Operator): 39 | """Show active image node image in the image editor""" 40 | bl_idname = "node.show_active_node_image" 41 | bl_label = "Show Active Node Node" 42 | bl_options = {"UNDO"} 43 | 44 | def execute(self, context): 45 | return {'FINISHED'} 46 | 47 | def invoke(self, context, event): 48 | mlocx = event.mouse_region_x 49 | mlocy = event.mouse_region_y 50 | select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False) 51 | 52 | if 'FINISHED' in select_node: # Only run if we're clicking on a node 53 | get_addon = "amaranth" in context.preferences.addons.keys() 54 | if not get_addon: 55 | return {"CANCELLED"} 56 | 57 | preferences = context.preferences.addons["amaranth"].preferences 58 | if preferences.use_image_node_display: 59 | if context.active_node: 60 | active_node = context.active_node 61 | 62 | if active_node.bl_idname in image_nodes: 63 | # Use largest image editor 64 | area = None 65 | area_size = 0 66 | for a in context.screen.areas: 67 | if a.type == "IMAGE_EDITOR": 68 | size = a.width * a.height 69 | if size > area_size: 70 | area_size = size 71 | area = a 72 | if area: 73 | for space in area.spaces: 74 | if space.type == "IMAGE_EDITOR": 75 | if active_node.bl_idname == "CompositorNodeViewer": 76 | space.image = bpy.data.images[ 77 | "Viewer Node"] 78 | elif active_node.bl_idname in ["CompositorNodeComposite", "CompositorNodeRLayers"]: 79 | space.image = bpy.data.images[ 80 | "Render Result"] 81 | elif active_node.image: 82 | space.image = active_node.image 83 | break 84 | else: 85 | return {'CANCELLED'} 86 | 87 | return {"FINISHED"} 88 | else: 89 | return {"PASS_THROUGH"} 90 | 91 | 92 | def register(): 93 | bpy.utils.register_class(AMTH_NODE_OT_show_active_node_image) 94 | kc = bpy.context.window_manager.keyconfigs.addon 95 | km = kc.keymaps.new(name="Node Editor", space_type="NODE_EDITOR") 96 | kmi = km.keymap_items.new("node.show_active_node_image", 97 | "LEFTMOUSE", "DOUBLE_CLICK") 98 | KEYMAPS.append((km, kmi)) 99 | 100 | 101 | def unregister(): 102 | bpy.utils.unregister_class(AMTH_NODE_OT_show_active_node_image) 103 | for km, kmi in KEYMAPS: 104 | km.keymap_items.remove(kmi) 105 | KEYMAPS.clear() 106 | -------------------------------------------------------------------------------- /node_editor/id_panel.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | 15 | """ 16 | Object / Material Indices Panel 17 | 18 | When working with ID Masks in the Nodes Editor, is hard to follow track 19 | of which objects/materials have which ID. 20 | This adds a panel on the sidebar when an ID Mask node is selected. 21 | The active object is highlighted between [square brackets] On the Nodes 22 | Editor's sidebar, when an ID Mask node is selected. 23 | """ 24 | 25 | import bpy 26 | 27 | 28 | class AMTH_NODE_PT_indices(bpy.types.Panel): 29 | bl_space_type = "NODE_EDITOR" 30 | bl_region_type = "UI" 31 | bl_label = "Object / Material Indices" 32 | bl_options = {"DEFAULT_CLOSED"} 33 | 34 | @classmethod 35 | def poll(cls, context): 36 | node = context.active_node 37 | return node and node.type == "ID_MASK" 38 | 39 | def draw(self, context): 40 | layout = self.layout 41 | 42 | objects = bpy.data.objects 43 | materials = bpy.data.materials 44 | node = context.active_node 45 | 46 | show_ob_id = False 47 | show_ma_id = False 48 | matching_ids = False 49 | 50 | if context.active_object: 51 | ob_act = context.active_object 52 | else: 53 | ob_act = False 54 | 55 | for ob in objects: 56 | if ob and ob.pass_index > 0: 57 | show_ob_id = True 58 | for ma in materials: 59 | if ma and ma.pass_index > 0: 60 | show_ma_id = True 61 | row = layout.row(align=True) 62 | row.prop(node, "index", text="Mask Index") 63 | row.prop(node, "use_matching_indices", text="Only Matching IDs") 64 | 65 | layout.separator() 66 | 67 | if not show_ob_id and not show_ma_id: 68 | layout.label( 69 | text="No objects or materials indices so far.", icon="INFO") 70 | 71 | if show_ob_id: 72 | split = layout.split() 73 | col = split.column() 74 | col.label(text="Object Name") 75 | split.label(text="ID Number") 76 | row = layout.row() 77 | for ob in objects: 78 | icon = "OUTLINER_DATA_" + ob.type 79 | if ob.library: 80 | icon = "LIBRARY_DATA_DIRECT" 81 | elif ob.is_library_indirect: 82 | icon = "LIBRARY_DATA_INDIRECT" 83 | 84 | if ob and node.use_matching_indices \ 85 | and ob.pass_index == node.index \ 86 | and ob.pass_index != 0: 87 | matching_ids = True 88 | row.label( 89 | text="[{}]".format(ob.name) 90 | if ob_act and ob.name == ob_act.name else ob.name, 91 | icon=icon) 92 | row.label(text="%s" % ob.pass_index) 93 | row = layout.row() 94 | 95 | elif ob and not node.use_matching_indices \ 96 | and ob.pass_index > 0: 97 | 98 | matching_ids = True 99 | row.label( 100 | text="[{}]".format(ob.name) 101 | if ob_act and ob.name == ob_act.name else ob.name, 102 | icon=icon) 103 | row.label(text="%s" % ob.pass_index) 104 | row = layout.row() 105 | 106 | if node.use_matching_indices and not matching_ids: 107 | row.label(text="No objects with ID %s" % 108 | node.index, icon="INFO") 109 | 110 | layout.separator() 111 | 112 | if show_ma_id: 113 | split = layout.split() 114 | col = split.column() 115 | col.label(text="Material Name") 116 | split.label(text="ID Number") 117 | row = layout.row() 118 | 119 | for ma in materials: 120 | icon = "BLANK1" 121 | if ma.use_nodes: 122 | icon = "NODETREE" 123 | elif ma.library: 124 | icon = "LIBRARY_DATA_DIRECT" 125 | if ma.is_library_indirect: 126 | icon = "LIBRARY_DATA_INDIRECT" 127 | 128 | if ma and node.use_matching_indices \ 129 | and ma.pass_index == node.index \ 130 | and ma.pass_index != 0: 131 | matching_ids = True 132 | row.label(text="%s" % ma.name, icon=icon) 133 | row.label(text="%s" % ma.pass_index) 134 | row = layout.row() 135 | 136 | elif ma and not node.use_matching_indices \ 137 | and ma.pass_index > 0: 138 | 139 | matching_ids = True 140 | row.label(text="%s" % ma.name, icon=icon) 141 | row.label(text="%s" % ma.pass_index) 142 | row = layout.row() 143 | 144 | if node.use_matching_indices and not matching_ids: 145 | row.label(text="No materials with ID %s" % 146 | node.index, icon="INFO") 147 | 148 | 149 | def register(): 150 | bpy.utils.register_class(AMTH_NODE_PT_indices) 151 | 152 | 153 | def unregister(): 154 | bpy.utils.unregister_class(AMTH_NODE_PT_indices) 155 | -------------------------------------------------------------------------------- /node_editor/node_shader_extra.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | import bpy 15 | 16 | 17 | # FEATURE: Shader Nodes Extra Info 18 | def node_shader_extra(self, context): 19 | if context.space_data.tree_type == 'ShaderNodeTree': 20 | ob = context.active_object 21 | snode = context.space_data 22 | layout = self.layout 23 | 24 | if ob and snode.shader_type == 'OBJECT': 25 | if ob.type == 'LAMP': 26 | layout.label(text="%s" % ob.name, 27 | icon="LAMP_%s" % ob.data.type) 28 | else: 29 | layout.label(text="%s" % ob.name, 30 | icon="OUTLINER_DATA_%s" % ob.type) 31 | 32 | # // FEATURE: Shader Nodes Extra Info 33 | 34 | 35 | def register(): 36 | bpy.types.NODE_HT_header.append(node_shader_extra) 37 | 38 | 39 | def unregister(): 40 | bpy.types.NODE_HT_header.remove(node_shader_extra) 41 | -------------------------------------------------------------------------------- /node_editor/node_stats.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Nodes Stats 16 | 17 | Display the number of selected and total nodes on the compositor. On the 18 | Compositing Nodes Editor. 19 | """ 20 | 21 | import bpy 22 | 23 | 24 | def node_stats(self, context): 25 | if context.scene.node_tree: 26 | tree_type = context.space_data.tree_type 27 | nodes = context.scene.node_tree.nodes 28 | nodes_total = len(nodes.keys()) 29 | nodes_selected = 0 30 | for n in nodes: 31 | if n.select: 32 | nodes_selected = nodes_selected + 1 33 | 34 | if tree_type == 'CompositorNodeTree': 35 | layout = self.layout 36 | row = layout.row(align=True) 37 | row.label(text="Nodes: %s/%s" % (nodes_selected, str(nodes_total))) 38 | 39 | 40 | def register(): 41 | bpy.types.NODE_HT_header.append(node_stats) 42 | 43 | 44 | def unregister(): 45 | bpy.types.NODE_HT_header.remove(node_stats) 46 | -------------------------------------------------------------------------------- /node_editor/normal_node.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Nodes: XYZ Sliders for Normal Node 16 | 17 | Tweak the Normal node more accurately by using these sliders. Not the most 18 | intuitive way to tweak, but it helps. 19 | 20 | ProTip: Hit Shift+Drag for moving in very small steps. 21 | 22 | Coded by Lukas Töenne. Thanks! 23 | Find it on the Properties panel, when selecting a Normal node. 24 | """ 25 | 26 | 27 | import bpy 28 | from mathutils import Vector 29 | 30 | 31 | # FEATURE: Normal Node Values, by Lukas Tönne 32 | def init(): 33 | prop_normal_vector = bpy.props.FloatVectorProperty( 34 | name="Normal", size=3, subtype='XYZ', 35 | min=-1.0, max=1.0, soft_min=-1.0, soft_max=1.0, 36 | get=normal_vector_get, set=normal_vector_set 37 | ) 38 | bpy.types.ShaderNodeNormal.normal_vector = prop_normal_vector 39 | bpy.types.CompositorNodeNormal.normal_vector = prop_normal_vector 40 | 41 | 42 | def clear(): 43 | del bpy.types.ShaderNodeNormal.normal_vector 44 | del bpy.types.CompositorNodeNormal.normal_vector 45 | 46 | 47 | def normal_vector_get(self): 48 | return self.outputs['Normal'].default_value 49 | 50 | 51 | def normal_vector_set(self, values): 52 | # default_value allows un-normalized values, 53 | # do this here to prevent awkward results 54 | values = Vector(values).normalized() 55 | self.outputs['Normal'].default_value = values 56 | 57 | 58 | def act_node(context): 59 | try: 60 | return context.active_node 61 | except AttributeError: 62 | return None 63 | 64 | 65 | def ui_node_normal_values(self, context): 66 | 67 | node = act_node(context) 68 | 69 | if act_node: 70 | if node and node.type == 'NORMAL': 71 | self.layout.prop(node, "normal_vector", text="") 72 | 73 | # // FEATURE: Normal Node Values, by Lukas Tönne 74 | 75 | 76 | def register(): 77 | init() 78 | bpy.types.NODE_PT_active_node_properties.append(ui_node_normal_values) 79 | 80 | 81 | def unregister(): 82 | bpy.types.NODE_PT_active_node_properties.remove(ui_node_normal_values) 83 | clear() 84 | -------------------------------------------------------------------------------- /node_editor/simplify_nodes.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Nodes Simplify Panel [WIP Feature] 16 | 17 | Disable/Enable certain nodes at a time. Useful to quickly "simplify" 18 | compositing. 19 | This feature is a work in progress, the main issue now is when switching 20 | many different kinds one after the other. 21 | 22 | On the Nodes Editor Properties N panel. 23 | """ 24 | 25 | import bpy 26 | 27 | 28 | def init(): 29 | nodes_compo_types = ( 30 | ("ALL", "All Types", "", 0), 31 | ("BLUR", "Blur", "", 1), 32 | ("BOKEHBLUR", "Bokeh Blur", "", 2), 33 | ("VECBLUR", "Vector Blur", "", 3), 34 | ("DEFOCUS", "Defocus", "", 4), 35 | ("R_LAYERS", "Render Layer", "", 5), 36 | ) 37 | node = bpy.types.Node 38 | nodes_compo = bpy.types.CompositorNodeTree 39 | nodes_compo.types = bpy.props.EnumProperty( 40 | items=nodes_compo_types, name="Types") 41 | nodes_compo.toggle_mute = bpy.props.BoolProperty(default=False) 42 | node.status = bpy.props.BoolProperty(default=False) 43 | 44 | 45 | def clear(): 46 | wm = bpy.context.window_manager 47 | for p in ("types", "toggle_mute", "status"): 48 | if wm.get(p): 49 | del wm[p] 50 | 51 | 52 | class AMTH_NODE_PT_simplify(bpy.types.Panel): 53 | 54 | bl_space_type = "NODE_EDITOR" 55 | bl_region_type = "UI" 56 | bl_label = "Simplify" 57 | bl_options = {"DEFAULT_CLOSED"} 58 | 59 | @classmethod 60 | def poll(cls, context): 61 | space = context.space_data 62 | return space.type == "NODE_EDITOR" \ 63 | and space.node_tree is not None \ 64 | and space.tree_type == "CompositorNodeTree" 65 | 66 | def draw(self, context): 67 | layout = self.layout 68 | node_tree = context.scene.node_tree 69 | 70 | if node_tree is not None: 71 | layout.prop(node_tree, "types") 72 | layout.operator(AMTH_NODE_OT_toggle_mute.bl_idname, 73 | text="Turn On" if node_tree.toggle_mute else "Turn Off", 74 | icon="RESTRICT_VIEW_OFF" if node_tree.toggle_mute else "RESTRICT_VIEW_ON") 75 | 76 | if node_tree.types == "VECBLUR": 77 | layout.label(text="This will also toggle the Vector pass {}".format( 78 | "on" if node_tree.toggle_mute else "off"), icon="INFO") 79 | 80 | 81 | class AMTH_NODE_OT_toggle_mute(bpy.types.Operator): 82 | 83 | bl_idname = "node.toggle_mute" 84 | bl_label = "Toggle Mute" 85 | 86 | def execute(self, context): 87 | scene = context.scene 88 | node_tree = scene.node_tree 89 | node_type = node_tree.types 90 | rlayers = scene.render 91 | 92 | if "amaranth_pass_vector" not in scene.keys(): 93 | scene["amaranth_pass_vector"] = [] 94 | 95 | # can"t extend() the list, so make a dummy one 96 | pass_vector = scene["amaranth_pass_vector"] 97 | 98 | if not pass_vector: 99 | pass_vector = [] 100 | 101 | if node_tree.toggle_mute: 102 | for node in node_tree.nodes: 103 | if node_type == "ALL": 104 | node.mute = node.status 105 | if node.type == node_type: 106 | node.mute = node.status 107 | if node_type == "VECBLUR": 108 | for layer in rlayers.layers: 109 | if layer.name in pass_vector: 110 | layer.use_pass_vector = True 111 | pass_vector.remove(layer.name) 112 | 113 | node_tree.toggle_mute = False 114 | 115 | else: 116 | for node in node_tree.nodes: 117 | if node_type == "ALL": 118 | node.mute = True 119 | if node.type == node_type: 120 | node.status = node.mute 121 | node.mute = True 122 | if node_type == "VECBLUR": 123 | for layer in rlayers.layers: 124 | if layer.use_pass_vector: 125 | pass_vector.append(layer.name) 126 | layer.use_pass_vector = False 127 | pass 128 | 129 | node_tree.toggle_mute = True 130 | 131 | # Write back to the custom prop 132 | pass_vector = sorted(set(pass_vector)) 133 | scene["amaranth_pass_vector"] = pass_vector 134 | 135 | return {"FINISHED"} 136 | 137 | 138 | def register(): 139 | init() 140 | bpy.utils.register_class(AMTH_NODE_PT_simplify) 141 | bpy.utils.register_class(AMTH_NODE_OT_toggle_mute) 142 | 143 | 144 | def unregister(): 145 | clear() 146 | bpy.utils.unregister_class(AMTH_NODE_PT_simplify) 147 | bpy.utils.unregister_class(AMTH_NODE_OT_toggle_mute) 148 | -------------------------------------------------------------------------------- /node_editor/switch_material.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Material Selector 16 | 17 | Quickly switch materials in the active mesh without going to the Properties editor 18 | 19 | Based on 'Afeitadora's work on Elysiun 20 | http://www.elysiun.com/forum/showthread.php?290097-Dynamic-Object-Dropdown-List&p=2361851#post2361851 21 | 22 | """ 23 | 24 | import bpy 25 | 26 | def ui_node_editor_material_select(self, context): 27 | 28 | act_ob = context.active_object 29 | 30 | if act_ob and context.active_object.type in {'MESH', 'CURVE', 'SURFACE', 'META'} and \ 31 | context.space_data.tree_type == 'ShaderNodeTree' and \ 32 | context.space_data.shader_type == 'OBJECT': 33 | 34 | if act_ob.active_material: 35 | mat_name = act_ob.active_material.name 36 | else: 37 | mat_name = "No Material" 38 | 39 | self.layout.operator_menu_enum("material.menu_select", 40 | "material_select", 41 | text=mat_name, 42 | icon="MATERIAL") 43 | 44 | class AMNodeEditorMaterialSelect(bpy.types.Operator): 45 | bl_idname = "material.menu_select" 46 | bl_label = "Select Material" 47 | bl_description = "Switch to another material in this mesh" 48 | 49 | def avail_materials(self,context): 50 | items = [(str(i),x.name,x.name, "MATERIAL", i) for i,x in enumerate(bpy.context.active_object.material_slots)] 51 | return items 52 | material_select: bpy.props.EnumProperty(items = avail_materials, name = "Available Materials") 53 | 54 | @classmethod 55 | def poll(cls, context): 56 | return context.active_object 57 | 58 | def execute(self,context): 59 | bpy.context.active_object.active_material_index = int(self.material_select) 60 | return {'FINISHED'} 61 | 62 | def register(): 63 | bpy.utils.register_class(AMNodeEditorMaterialSelect) 64 | bpy.types.NODE_HT_header.append(ui_node_editor_material_select) 65 | 66 | def unregister(): 67 | bpy.utils.unregister_class(AMNodeEditorMaterialSelect) 68 | bpy.types.NODE_HT_header.remove(ui_node_editor_material_select) 69 | -------------------------------------------------------------------------------- /node_editor/templates/__init__.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Node Templates - Vignette, Vector Blur 16 | 17 | Add a set of nodes with one click, in this version I added a "Vignette" 18 | as first example. 19 | 20 | There is no official way to make a vignette, this is just my approach at 21 | it. Templates: On the Compositor's header, "Template" pulldown. Or hit W. 22 | Vignette: Adjust the size and position of the vignette with the Ellipse 23 | Mask's X/Y and width, height values. 24 | """ 25 | 26 | import bpy 27 | from amaranth.node_editor.templates.vectorblur import AMTH_NODE_OT_AddTemplateVectorBlur 28 | from amaranth.node_editor.templates.vignette import AMTH_NODE_OT_AddTemplateVignette 29 | 30 | 31 | KEYMAPS = list() 32 | 33 | 34 | # Node Templates Menu 35 | class AMTH_NODE_MT_amaranth_templates(bpy.types.Menu): 36 | bl_idname = 'AMTH_NODE_MT_amaranth_templates' 37 | bl_space_type = 'NODE_EDITOR' 38 | bl_label = "Templates" 39 | bl_description = "List of Amaranth Templates" 40 | 41 | def draw(self, context): 42 | layout = self.layout 43 | layout.operator( 44 | AMTH_NODE_OT_AddTemplateVectorBlur.bl_idname, 45 | text="Vector Blur", 46 | icon='FORCE_HARMONIC') 47 | layout.operator( 48 | AMTH_NODE_OT_AddTemplateVignette.bl_idname, 49 | text="Vignette", 50 | icon='COLOR') 51 | 52 | 53 | def node_templates_pulldown(self, context): 54 | if context.space_data.tree_type == 'CompositorNodeTree': 55 | layout = self.layout 56 | row = layout.row(align=True) 57 | row.scale_x = 1.3 58 | row.menu("AMTH_NODE_MT_amaranth_templates", 59 | icon="NODETREE") 60 | 61 | 62 | def register(): 63 | bpy.utils.register_class(AMTH_NODE_MT_amaranth_templates) 64 | bpy.utils.register_class(AMTH_NODE_OT_AddTemplateVignette) 65 | bpy.utils.register_class(AMTH_NODE_OT_AddTemplateVectorBlur) 66 | bpy.types.NODE_HT_header.append(node_templates_pulldown) 67 | kc = bpy.context.window_manager.keyconfigs.addon 68 | km = kc.keymaps.new(name="Node Editor", space_type="NODE_EDITOR") 69 | kmi = km.keymap_items.new("wm.call_menu", "W", "PRESS") 70 | kmi.properties.name = "AMTH_NODE_MT_amaranth_templates" 71 | KEYMAPS.append((km, kmi)) 72 | 73 | 74 | def unregister(): 75 | bpy.utils.unregister_class(AMTH_NODE_MT_amaranth_templates) 76 | bpy.utils.unregister_class(AMTH_NODE_OT_AddTemplateVignette) 77 | bpy.utils.unregister_class(AMTH_NODE_OT_AddTemplateVectorBlur) 78 | bpy.types.NODE_HT_header.remove(node_templates_pulldown) 79 | for km, kmi in KEYMAPS: 80 | km.keymap_items.remove(kmi) 81 | KEYMAPS.clear() 82 | -------------------------------------------------------------------------------- /node_editor/templates/vectorblur.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | 15 | import bpy 16 | from mathutils import Vector 17 | 18 | 19 | class AMTH_NODE_OT_AddTemplateVectorBlur(bpy.types.Operator): 20 | bl_idname = "node.template_add_vectorblur" 21 | bl_label = "Add Vector Blur" 22 | bl_description = "Add a vector blur filter" 23 | bl_options = {"REGISTER", "UNDO"} 24 | 25 | @classmethod 26 | def poll(cls, context): 27 | space = context.space_data 28 | tree = context.scene.node_tree 29 | return space.type == "NODE_EDITOR" \ 30 | and space.node_tree is not None \ 31 | and space.tree_type == "CompositorNodeTree" \ 32 | and tree \ 33 | and tree.nodes.active \ 34 | and tree.nodes.active.type == "R_LAYERS" 35 | 36 | def _setupNodes(self, context): 37 | scene = context.scene 38 | space = context.space_data 39 | tree = scene.node_tree 40 | 41 | bpy.ops.node.select_all(action="DESELECT") 42 | 43 | act_node = tree.nodes.active 44 | rlayer = act_node.scene.render.layers[act_node.layer] 45 | 46 | if not rlayer.use_pass_vector: 47 | rlayer.use_pass_vector = True 48 | 49 | vblur = tree.nodes.new(type="CompositorNodeVecBlur") 50 | vblur.use_curved = True 51 | vblur.factor = 0.5 52 | 53 | tree.links.new(act_node.outputs["Image"], vblur.inputs["Image"]) 54 | tree.links.new(act_node.outputs["Z"], vblur.inputs["Z"]) 55 | tree.links.new(act_node.outputs["Speed"], vblur.inputs["Speed"]) 56 | 57 | if tree.nodes.active: 58 | vblur.location = tree.nodes.active.location 59 | vblur.location += Vector((250.0, 0.0)) 60 | else: 61 | vblur.location += Vector( 62 | (space.cursor_location[0], space.cursor_location[1])) 63 | 64 | vblur.select = True 65 | 66 | def execute(self, context): 67 | self._setupNodes(context) 68 | 69 | return {"FINISHED"} 70 | -------------------------------------------------------------------------------- /node_editor/templates/vignette.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | 15 | import bpy 16 | from mathutils import Vector 17 | 18 | 19 | class AMTH_NODE_OT_AddTemplateVignette(bpy.types.Operator): 20 | bl_idname = "node.template_add_vignette" 21 | bl_label = "Add Vignette" 22 | bl_description = "Add a vignette effect" 23 | bl_options = {"REGISTER", "UNDO"} 24 | 25 | @classmethod 26 | def poll(cls, context): 27 | space = context.space_data 28 | return space.type == "NODE_EDITOR" \ 29 | and space.node_tree is not None \ 30 | and space.tree_type == "CompositorNodeTree" 31 | 32 | # used as reference the setup scene script from master nazgul 33 | def _setupNodes(self, context): 34 | scene = context.scene 35 | space = context.space_data 36 | tree = scene.node_tree 37 | has_act = True if tree.nodes.active else False 38 | 39 | bpy.ops.node.select_all(action="DESELECT") 40 | 41 | ellipse = tree.nodes.new(type="CompositorNodeEllipseMask") 42 | ellipse.width = 0.8 43 | ellipse.height = 0.4 44 | blur = tree.nodes.new(type="CompositorNodeBlur") 45 | blur.use_relative = True 46 | blur.factor_x = 30 47 | blur.factor_y = 50 48 | ramp = tree.nodes.new(type="CompositorNodeValToRGB") 49 | ramp.color_ramp.interpolation = "B_SPLINE" 50 | ramp.color_ramp.elements[1].color = (0.6, 0.6, 0.6, 1) 51 | 52 | overlay = tree.nodes.new(type="CompositorNodeMixRGB") 53 | overlay.blend_type = "OVERLAY" 54 | overlay.inputs[0].default_value = 0.8 55 | overlay.inputs[1].default_value = (0.5, 0.5, 0.5, 1) 56 | 57 | tree.links.new(ellipse.outputs["Mask"], blur.inputs["Image"]) 58 | tree.links.new(blur.outputs["Image"], ramp.inputs[0]) 59 | tree.links.new(ramp.outputs["Image"], overlay.inputs[2]) 60 | if has_act: 61 | tree.links.new(tree.nodes.active.outputs[0], overlay.inputs[1]) 62 | 63 | if has_act: 64 | overlay.location = tree.nodes.active.location 65 | overlay.location += Vector((350.0, 0.0)) 66 | else: 67 | overlay.location += Vector( 68 | (space.cursor_location[0], space.cursor_location[1])) 69 | 70 | ellipse.location = overlay.location 71 | ellipse.location += Vector((-715.0, -400)) 72 | ellipse.inputs[0].hide = True 73 | ellipse.inputs[1].hide = True 74 | 75 | blur.location = ellipse.location 76 | blur.location += Vector((300.0, 0.0)) 77 | blur.inputs["Size"].hide = True 78 | 79 | ramp.location = blur.location 80 | ramp.location += Vector((175.0, 0)) 81 | ramp.outputs["Alpha"].hide = True 82 | 83 | for node in (ellipse, blur, ramp, overlay): 84 | node.select = True 85 | node.show_preview = False 86 | 87 | bpy.ops.node.join() 88 | 89 | frame = ellipse.parent 90 | frame.label = "Vignette" 91 | frame.use_custom_color = True 92 | frame.color = (0.1, 0.1, 0.1) 93 | 94 | overlay.parent = None 95 | overlay.label = "Vignette Overlay" 96 | 97 | def execute(self, context): 98 | self._setupNodes(context) 99 | 100 | return {"FINISHED"} 101 | -------------------------------------------------------------------------------- /prefs.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | 15 | import bpy 16 | from bpy.props import ( 17 | BoolProperty, 18 | IntProperty, 19 | ) 20 | 21 | 22 | class AmaranthToolsetPreferences(bpy.types.AddonPreferences): 23 | bl_idname = "amaranth" 24 | use_frame_current: BoolProperty( 25 | name="Current Frame Slider", 26 | description="Set the current frame from the Specials menu in the 3D View", 27 | default=True, 28 | ) 29 | use_file_save_reload: BoolProperty( 30 | name="Save & Reload File", 31 | description="File menu > Save & Reload, or Ctrl + Shift + W", 32 | default=True, 33 | ) 34 | use_scene_refresh: BoolProperty( 35 | name="Refresh Scene", 36 | description="Specials Menu [W]", 37 | default=True, 38 | ) 39 | use_timeline_extra_info: BoolProperty( 40 | name="Timeline Extra Info", 41 | description="Timeline Header", 42 | default=True, 43 | ) 44 | use_image_node_display: BoolProperty( 45 | name="Active Image Node in Editor", 46 | description="Display active node image in image editor", 47 | default=True, 48 | ) 49 | use_scene_stats: BoolProperty( 50 | name="Extra Scene Statistics", 51 | description="Display extra scene statistics in the status bar (may be slow in heavy scenes)", 52 | default=False, 53 | ) 54 | frames_jump: IntProperty( 55 | name="Frames", 56 | description="Number of frames to jump forward/backward", 57 | default=10, 58 | min=1 59 | ) 60 | use_framerate: BoolProperty( 61 | name="Framerate Jump", 62 | description="Jump the amount of frames forward/backward that you have set as your framerate", 63 | default=False, 64 | ) 65 | use_layers_for_render: BoolProperty( 66 | name="Current Layers for Render", 67 | description="Save the layers that should be enabled for render", 68 | default=True, 69 | ) 70 | 71 | def draw(self, context): 72 | layout = self.layout 73 | 74 | layout.label( 75 | text="Here you can enable or disable specific tools, " 76 | "in case they interfere with others or are just plain annoying") 77 | 78 | split = layout.split(factor=0.25) 79 | 80 | col = split.column() 81 | sub = col.column(align=True) 82 | sub.label(text="3D View", icon="VIEW3D") 83 | sub.prop(self, "use_frame_current") 84 | sub.prop(self, "use_scene_refresh") 85 | 86 | sub.separator() 87 | 88 | sub.label(text="General", icon="SCENE_DATA") 89 | sub.prop(self, "use_file_save_reload") 90 | sub.prop(self, "use_timeline_extra_info") 91 | sub.prop(self, "use_scene_stats") 92 | sub.prop(self, "use_layers_for_render") 93 | sub.prop(self, "use_framerate") 94 | 95 | sub.separator() 96 | 97 | sub.label(text="Nodes Editor", icon="NODETREE") 98 | sub.prop(self, "use_image_node_display") 99 | 100 | col = split.column() 101 | sub = col.column(align=True) 102 | sub.label(text="") 103 | sub.label( 104 | text="Set the current frame from the Specials menu in the 3D View [W]") 105 | sub.label( 106 | text="Refresh the current Scene. Hotkey: F5 or in Specials menu [W]") 107 | 108 | sub.separator() 109 | sub.label(text="") # General icon 110 | sub.label( 111 | text="Quickly save and reload the current file (no warning!). " 112 | "File menu or Ctrl+Shift+W") 113 | sub.label( 114 | text="SMPTE Timecode and frames left/ahead on Timeline's header") 115 | sub.label( 116 | text="Display extra stats for Scenes, Cameras, Meshlights (Cycles). Can be slow in heavy scenes") 117 | sub.label( 118 | text="Save the set of layers that should be activated for a final render") 119 | sub.label( 120 | text="Jump the amount of frames forward/backward that you've set as your framerate") 121 | 122 | sub.separator() 123 | sub.label(text="") # Nodes 124 | sub.label( 125 | text="When double-clicking an Image node, display it on the Image editor " 126 | "(if any)") 127 | 128 | 129 | def register(): 130 | bpy.utils.register_class(AmaranthToolsetPreferences) 131 | 132 | 133 | def unregister(): 134 | bpy.utils.unregister_class(AmaranthToolsetPreferences) 135 | -------------------------------------------------------------------------------- /render/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/venomgfx/amaranth/e2a34ae7fb86f66ac931a40cdc9d20d0f2ebb583/render/__init__.py -------------------------------------------------------------------------------- /render/border_camera.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Set Camera Bounds as Render Border 16 | 17 | When in camera view, we can now set the border-render to be the same size 18 | of the camera, so we don't render outside the view. Makes faster render 19 | preview. Under Specials menu W, when in Camera view. 20 | """ 21 | 22 | import bpy 23 | 24 | 25 | class AMTH_VIEW3D_OT_render_border_camera(bpy.types.Operator): 26 | 27 | """Set camera bounds as render border""" 28 | bl_idname = "view3d.render_border_camera" 29 | bl_label = "Camera as Render Border" 30 | 31 | @classmethod 32 | def poll(cls, context): 33 | return context.space_data.region_3d.view_perspective == "CAMERA" 34 | 35 | def execute(self, context): 36 | render = context.scene.render 37 | render.use_border = True 38 | render.border_min_x = 0 39 | render.border_min_y = 0 40 | render.border_max_x = 1 41 | render.border_max_y = 1 42 | 43 | return {"FINISHED"} 44 | 45 | 46 | def button_render_border_camera(self, context): 47 | view3d = context.space_data.region_3d 48 | 49 | if view3d.view_perspective == "CAMERA": 50 | layout = self.layout 51 | layout.separator() 52 | layout.operator(AMTH_VIEW3D_OT_render_border_camera.bl_idname, 53 | text="Camera as Render Border", 54 | icon="FULLSCREEN_ENTER") 55 | 56 | 57 | def register(): 58 | bpy.utils.register_class(AMTH_VIEW3D_OT_render_border_camera) 59 | bpy.types.VIEW3D_MT_object_context_menu.append(button_render_border_camera) 60 | 61 | 62 | def unregister(): 63 | bpy.utils.unregister_class(AMTH_VIEW3D_OT_render_border_camera) 64 | bpy.types.VIEW3D_MT_object_context_menu.remove(button_render_border_camera) 65 | -------------------------------------------------------------------------------- /render/final_resolution.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | UI: Final Resolution 16 | 17 | Always wondered how big the render was going to be when rendering at a 18 | certain %? 19 | This feature displays a "Final Resolution" label with the size in pixels 20 | of your render, it also displays the size for border renders. 21 | 22 | On the 'Dimensions' panel, Render properties. 23 | """ 24 | import bpy 25 | 26 | 27 | def render_final_resolution_ui(self, context): 28 | 29 | rd = context.scene.render 30 | layout = self.layout 31 | 32 | final_res_x = (rd.resolution_x * rd.resolution_percentage) / 100 33 | final_res_y = (rd.resolution_y * rd.resolution_percentage) / 100 34 | 35 | if rd.use_border: 36 | final_res_x_border = round( 37 | (final_res_x * (rd.border_max_x - rd.border_min_x))) 38 | final_res_y_border = round( 39 | (final_res_y * (rd.border_max_y - rd.border_min_y))) 40 | layout.label(text="Final Resolution: {} x {} [Border: {} x {}]".format( 41 | str(final_res_x)[:-2], str(final_res_y)[:-2], 42 | str(final_res_x_border), str(final_res_y_border))) 43 | else: 44 | layout.label(text="Final Resolution: {} x {}".format( 45 | str(final_res_x)[:-2], str(final_res_y)[:-2])) 46 | 47 | 48 | def register(): 49 | bpy.types.RENDER_PT_dimensions.append(render_final_resolution_ui) 50 | 51 | 52 | def unregister(): 53 | bpy.types.RENDER_PT_dimensions.remove(render_final_resolution_ui) 54 | -------------------------------------------------------------------------------- /render/meshlight_add.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | from amaranth.utils import cycles_exists 4 | 5 | 6 | # FEATURE: Add Meshlight 7 | class AMTH_OBJECT_OT_meshlight_add(bpy.types.Operator): 8 | 9 | """Add a light emitting mesh""" 10 | bl_idname = "object.meshlight_add" 11 | bl_label = "Add Meshlight" 12 | bl_options = {'REGISTER', 'UNDO'} 13 | 14 | single_sided: bpy.props.BoolProperty( 15 | name="Single Sided", 16 | default=True, 17 | description="Only emit light on one side", 18 | ) 19 | 20 | is_constant: bpy.props.BoolProperty( 21 | name="Constant Falloff", 22 | default=False, 23 | description="Energy is constant (i.e. the Sun), " 24 | "independent of how close to the source you are", 25 | ) 26 | 27 | visible: bpy.props.BoolProperty( 28 | name="Visible on Camera", 29 | default=False, 30 | description="Whether to show the meshlight source on Camera", 31 | ) 32 | 33 | size: bpy.props.FloatProperty( 34 | name="Size", 35 | description="Meshlight size. Lower is sharper shadows, higher is softer", 36 | min=0.01, max=100.0, 37 | default=1.0, 38 | ) 39 | 40 | strength: bpy.props.FloatProperty( 41 | name="Strength", 42 | min=0.01, max=100000.0, 43 | default=1.5, 44 | step=0.25, 45 | ) 46 | 47 | temperature: bpy.props.FloatProperty( 48 | name="Temperature", 49 | min=800, max=12000.0, 50 | default=5500.0, 51 | step=800.0, 52 | description="Temperature in Kelvin. Lower is warmer, higher is colder", 53 | ) 54 | 55 | rotation: bpy.props.FloatVectorProperty( 56 | name="Rotation", 57 | subtype='EULER', 58 | ) 59 | 60 | def execute(self, context): 61 | scene = context.scene 62 | # exists = False 63 | number = 1 64 | 65 | for obs in bpy.data.objects: 66 | if obs.name.startswith("light_meshlight"): 67 | number += 1 68 | 69 | meshlight_name = 'light_meshlight_%.2d' % number 70 | 71 | bpy.ops.mesh.primitive_grid_add( 72 | x_subdivisions=4, y_subdivisions=4, 73 | rotation=self.rotation, size=self.size) 74 | 75 | bpy.context.object.name = meshlight_name 76 | meshlight = scene.objects[meshlight_name] 77 | meshlight.show_wire = True 78 | meshlight.show_all_edges = True 79 | 80 | material = bpy.data.materials.get(meshlight_name) 81 | 82 | if not material: 83 | material = bpy.data.materials.new(meshlight_name) 84 | 85 | bpy.ops.object.material_slot_add() 86 | meshlight.active_material = material 87 | 88 | material.use_nodes = True 89 | material.diffuse_color = (1, 0.5, 0, 1) 90 | nodes = material.node_tree.nodes 91 | links = material.node_tree.links 92 | 93 | # clear default nodes to start nice fresh 94 | for no in nodes: 95 | nodes.remove(no) 96 | 97 | if self.single_sided: 98 | geometry = nodes.new(type="ShaderNodeNewGeometry") 99 | 100 | transparency = nodes.new(type="ShaderNodeBsdfTransparent") 101 | transparency.inputs[0].default_value = (1, 1, 1, 1) 102 | transparency.location = geometry.location 103 | transparency.location += Vector((0.0, -55.0)) 104 | 105 | emission = nodes.new(type="ShaderNodeEmission") 106 | emission.inputs['Strength'].default_value = self.strength 107 | emission.location = transparency.location 108 | emission.location += Vector((0.0, -80.0)) 109 | 110 | blackbody = nodes.new(type="ShaderNodeBlackbody") 111 | blackbody.inputs['Temperature'].default_value = self.temperature 112 | blackbody.location = emission.location 113 | blackbody.location += Vector((-180.0, 0.0)) 114 | blackbody.label = 'Temperature' 115 | 116 | mix = nodes.new(type="ShaderNodeMixShader") 117 | mix.location = geometry.location 118 | mix.location += Vector((180.0, 0.0)) 119 | mix.inputs[2].show_expanded = True 120 | 121 | output = nodes.new(type="ShaderNodeOutputMaterial") 122 | output.inputs[1].hide = True 123 | output.inputs[2].hide = True 124 | output.location = mix.location 125 | output.location += Vector((180.0, 0.0)) 126 | 127 | # Make links 128 | links.new(geometry.outputs['Backfacing'], mix.inputs[0]) 129 | links.new(transparency.outputs['BSDF'], mix.inputs[1]) 130 | links.new(emission.outputs['Emission'], mix.inputs[2]) 131 | links.new(blackbody.outputs['Color'], emission.inputs['Color']) 132 | links.new(mix.outputs['Shader'], output.inputs['Surface']) 133 | 134 | for sockets in geometry.outputs: 135 | sockets.hide = True 136 | else: 137 | emission = nodes.new(type="ShaderNodeEmission") 138 | emission.inputs['Strength'].default_value = self.strength 139 | 140 | blackbody = nodes.new(type="ShaderNodeBlackbody") 141 | blackbody.inputs['Temperature'].default_value = self.temperature 142 | blackbody.location = emission.location 143 | blackbody.location += Vector((-180.0, 0.0)) 144 | blackbody.label = 'Temperature' 145 | 146 | output = nodes.new(type="ShaderNodeOutputMaterial") 147 | output.inputs[1].hide = True 148 | output.inputs[2].hide = True 149 | output.location = emission.location 150 | output.location += Vector((180.0, 0.0)) 151 | 152 | links.new(blackbody.outputs['Color'], emission.inputs['Color']) 153 | links.new(emission.outputs['Emission'], output.inputs['Surface']) 154 | 155 | if self.is_constant: 156 | falloff = nodes.new(type="ShaderNodeLightFalloff") 157 | falloff.inputs['Strength'].default_value = self.strength 158 | falloff.location = emission.location 159 | falloff.location += Vector((-180.0, -80.0)) 160 | 161 | links.new(falloff.outputs['Constant'], emission.inputs['Strength']) 162 | 163 | for sockets in falloff.outputs: 164 | sockets.hide = True 165 | 166 | # so it shows slider on properties editor 167 | for sockets in emission.inputs: 168 | sockets.show_expanded = True 169 | 170 | material.cycles.sample_as_light = True 171 | meshlight.cycles_visibility.shadow = False 172 | meshlight.cycles_visibility.camera = self.visible 173 | 174 | return {'FINISHED'} 175 | 176 | 177 | def ui_menu_lamps_add(self, context): 178 | if cycles_exists() and context.scene.render.engine == 'CYCLES': 179 | self.layout.separator() 180 | self.layout.operator( 181 | AMTH_OBJECT_OT_meshlight_add.bl_idname, 182 | icon="LIGHT_AREA", text="Meshlight") 183 | 184 | # //FEATURE: Add Meshlight: Single Sided 185 | 186 | 187 | def register(): 188 | bpy.utils.register_class(AMTH_OBJECT_OT_meshlight_add) 189 | bpy.types.VIEW3D_MT_light_add.append(ui_menu_lamps_add) 190 | 191 | 192 | def unregister(): 193 | bpy.utils.unregister_class(AMTH_OBJECT_OT_meshlight_add) 194 | bpy.types.VIEW3D_MT_light_add.remove(ui_menu_lamps_add) 195 | -------------------------------------------------------------------------------- /render/meshlight_select.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Select Meshlights 16 | 17 | Select all the meshes that emit light. On the header of the 3D View, top 18 | of the select menu. 19 | """ 20 | 21 | import bpy 22 | from amaranth import utils 23 | 24 | 25 | class AMTH_OBJECT_OT_select_meshlights(bpy.types.Operator): 26 | 27 | """Select light emitting meshes""" 28 | bl_idname = "object.select_meshlights" 29 | bl_label = "Select Meshlights" 30 | bl_options = {"UNDO"} 31 | 32 | @classmethod 33 | def poll(cls, context): 34 | return context.scene.render.engine == "CYCLES" 35 | 36 | def execute(self, context): 37 | # Deselect everything first 38 | bpy.ops.object.select_all(action="DESELECT") 39 | 40 | for ob in context.scene.objects: 41 | if utils.cycles_is_emission(context, ob): 42 | ob.select_set(True) 43 | context.view_layer.objects.active = ob 44 | 45 | if not context.selected_objects and not context.view_layer.objects.active: 46 | self.report({"INFO"}, "No meshlights to select") 47 | 48 | return {"FINISHED"} 49 | 50 | 51 | def button_select_meshlights(self, context): 52 | if utils.cycles_exists() and utils.cycles_active(context): 53 | self.layout.operator('object.select_meshlights', icon="LAMP_SUN") 54 | 55 | 56 | def register(): 57 | bpy.utils.register_class(AMTH_OBJECT_OT_select_meshlights) 58 | bpy.types.VIEW3D_MT_select_object.append(button_select_meshlights) 59 | 60 | 61 | def unregister(): 62 | bpy.utils.unregister_class(AMTH_OBJECT_OT_select_meshlights) 63 | bpy.types.VIEW3D_MT_select_object.remove(button_select_meshlights) 64 | -------------------------------------------------------------------------------- /render/only_render.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | 3D View Shading Shortcuts 16 | 17 | Two new shortcuts have been added to the 3D View. 18 | Shift+Z Rendered Preview (now in Blender 2.70) 19 | Alt+Shift+Z Only Render 20 | """ 21 | 22 | import bpy 23 | 24 | 25 | KEYMAPS = list() 26 | 27 | 28 | class AMTH_VIEW3D_OT_show_only_render(bpy.types.Operator): 29 | bl_idname = "view3d.show_only_render" 30 | bl_label = "Show Only Render" 31 | 32 | def execute(self, context): 33 | space = bpy.context.space_data 34 | 35 | if space.show_only_render: 36 | space.show_only_render = False 37 | else: 38 | space.show_only_render = True 39 | return {"FINISHED"} 40 | 41 | 42 | def register(): 43 | bpy.utils.register_class(AMTH_VIEW3D_OT_show_only_render) 44 | kc = bpy.context.window_manager.keyconfigs.addon 45 | km = kc.keymaps.new(name="3D View", space_type="VIEW_3D") 46 | kmi = km.keymap_items.new("view3d.show_only_render", "Z", "PRESS", 47 | shift=True, alt=True) 48 | KEYMAPS.append((km, kmi)) 49 | 50 | 51 | def unregister(): 52 | bpy.utils.unregister_class(AMTH_VIEW3D_OT_show_only_render) 53 | for km, kmi in KEYMAPS: 54 | km.keymap_items.remove(kmi) 55 | KEYMAPS.clear() 56 | -------------------------------------------------------------------------------- /render/passepartout.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Passepartout on Specials menu 16 | 17 | The passepartout value of local cameras is now available on the Specials 18 | menu for easy access. 19 | Under Specials menu W, when in Camera view. 20 | """ 21 | 22 | import bpy 23 | 24 | 25 | def button_camera_passepartout(self, context): 26 | view3d = context.space_data.region_3d 27 | cam = context.scene.camera 28 | 29 | if view3d.view_perspective == "CAMERA": 30 | if cam is None or not hasattr(cam, "data") or cam.type != "CAMERA": 31 | return 32 | 33 | layout = self.layout 34 | if cam.data.show_passepartout: 35 | layout.prop(cam.data, "passepartout_alpha", text="Passepartout") 36 | else: 37 | layout.prop(cam.data, "show_passepartout") 38 | 39 | 40 | def register(): 41 | bpy.types.VIEW3D_MT_object_context_menu.append(button_camera_passepartout) 42 | 43 | 44 | def unregister(): 45 | bpy.types.VIEW3D_MT_object_context_menu.remove(button_camera_passepartout) 46 | -------------------------------------------------------------------------------- /render/remember_layers.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Remember Layers for Render 16 | If you're doing lighting/rendering tasks, you'll probably have a bunch of 17 | layers that you want/need to be enabled for final renders. 18 | 19 | When tweaking lighting, or if somebody else has to open the file to check 20 | something, you/they are likely to tweak the layers visibility and forget 21 | which ones were needed for the render to look good. 22 | 23 | In the Render Layers properties, you'll now find a "Save Current Layers for 24 | Render" button, this will save the currently visible scene layers as those 25 | that should be enabled for render. You can adjust this further by clicking 26 | on the slots on the right. 27 | 28 | Now all you need to do before saving your file for rendering is press the 29 | "View Layers for Render". Find it on the Render Layers properties. 30 | """ 31 | 32 | import bpy 33 | 34 | 35 | # FEATURE: Set Layers to Render 36 | class AMTH_SCENE_OT_layers_render_save(bpy.types.Operator): 37 | 38 | """Save the current scene layers as those that should be enabled for final renders""" 39 | bl_idname = "scene.amaranth_layers_render_save" 40 | bl_label = "Save as Layers for Render" 41 | 42 | def execute(self, context): 43 | which = [] 44 | n = -1 45 | 46 | for l in context.scene.layers: 47 | n += 1 48 | if l: 49 | which.append(n) 50 | 51 | context.scene["amth_layers_for_render"] = which 52 | self.report({"INFO"}, "Layers for Render Saved") 53 | 54 | return {"FINISHED"} 55 | 56 | 57 | class AMTH_SCENE_OT_layers_render_view(bpy.types.Operator): 58 | 59 | """Enable the scene layers that should be active for final renders""" 60 | bl_idname = "scene.amaranth_layers_render_view" 61 | bl_label = "View Layers for Render" 62 | 63 | def execute(self, context): 64 | scene = context.scene 65 | layers_render = scene["amth_layers_for_render"] 66 | 67 | for window in bpy.context.window_manager.windows: 68 | screen = window.screen 69 | 70 | for area in screen.areas: 71 | if area.type == "VIEW_3D": 72 | override = { 73 | "window": window, 74 | "screen": screen, 75 | "scene": scene, 76 | "area": area, 77 | "region": area.regions[4], 78 | "blend_data": context.blend_data} 79 | 80 | if layers_render: 81 | bpy.ops.view3d.layers( 82 | override, 83 | nr=layers_render[0]+1, 84 | extend=False, 85 | toggle=False) 86 | 87 | for n in layers_render: 88 | context.scene.layers[n] = True 89 | else: 90 | bpy.ops.view3d.layers( 91 | override, 92 | nr=1, 93 | extend=False, 94 | toggle=False) 95 | self.report({"INFO"}, "No layers set for render") 96 | 97 | break 98 | 99 | return {"FINISHED"} 100 | 101 | 102 | class AMTH_SCENE_OT_layers_render_set_individual(bpy.types.Operator): 103 | 104 | """Whether this layer should be enabled or not for final renders""" 105 | bl_idname = "scene.amaranth_layers_render_set_individual" 106 | bl_label = "Set This Layer for Render" 107 | 108 | toggle = bpy.props.BoolProperty() 109 | number = bpy.props.IntProperty() 110 | 111 | def execute(self, context): 112 | number = self.number 113 | 114 | new_layers = [] 115 | 116 | for la in context.scene["amth_layers_for_render"]: 117 | new_layers.append(la) 118 | 119 | if len(context.scene["amth_layers_for_render"]) and number in new_layers: 120 | new_layers.remove(number) 121 | else: 122 | new_layers.append(number) 123 | 124 | # Remove Duplicates 125 | new_layers = list(set(new_layers)) 126 | context.scene["amth_layers_for_render"] = new_layers 127 | 128 | bpy.ops.scene.amaranth_layers_render_view() 129 | 130 | return {"FINISHED"} 131 | 132 | 133 | class AMTH_SCENE_OT_layers_render_clear(bpy.types.Operator): 134 | 135 | """Clear layers for render""" 136 | bl_idname = "scene.amaranth_layers_render_clear" 137 | bl_label = "Clear Layers for Render" 138 | 139 | def execute(self, context): 140 | 141 | if context.scene.get("amth_layers_for_render"): 142 | context.scene["amth_layers_for_render"] = [] 143 | 144 | return {"FINISHED"} 145 | 146 | 147 | def ui_layers_for_render(self, context): 148 | 149 | preferences = context.user_preferences.addons["amaranth"].preferences 150 | 151 | if preferences.use_layers_for_render: 152 | lfr_available = context.scene.get("amth_layers_for_render") 153 | if lfr_available: 154 | lfr = context.scene["amth_layers_for_render"] 155 | 156 | layout = self.layout 157 | layout.label("Layers for Rendering:") 158 | split = layout.split() 159 | col = split.column(align=True) 160 | row = col.row(align=True) 161 | row.operator( 162 | AMTH_SCENE_OT_layers_render_save.bl_idname, 163 | text="Replace Layers" if lfr_available else "Save Current Layers for Render", 164 | icon="FILE_REFRESH" if lfr_available else "LAYER_USED") 165 | 166 | if lfr_available: 167 | row.operator( 168 | AMTH_SCENE_OT_layers_render_clear.bl_idname, 169 | icon="X", text="") 170 | col = col.column(align=True) 171 | col.enabled = True if lfr_available else False 172 | col.operator( 173 | AMTH_SCENE_OT_layers_render_view.bl_idname, 174 | icon="RESTRICT_VIEW_OFF") 175 | 176 | split = split.split() 177 | col = split.column(align=True) 178 | row = col.row(align=True) 179 | 180 | for n in range(0, 5): 181 | row.operator( 182 | AMTH_SCENE_OT_layers_render_set_individual.bl_idname, 183 | text="", 184 | icon="LAYER_ACTIVE" if n in lfr else "BLANK1").number = n 185 | row = col.row(align=True) 186 | for n in range(10, 15): 187 | row.operator( 188 | AMTH_SCENE_OT_layers_render_set_individual.bl_idname, 189 | text="", 190 | icon="LAYER_ACTIVE" if n in lfr else "BLANK1").number = n 191 | 192 | split = split.split() 193 | col = split.column(align=True) 194 | row = col.row(align=True) 195 | 196 | for n in range(5, 10): 197 | row.operator( 198 | AMTH_SCENE_OT_layers_render_set_individual.bl_idname, 199 | text="", 200 | icon="LAYER_ACTIVE" if n in lfr else "BLANK1").number = n 201 | row = col.row(align=True) 202 | for n in range(15, 20): 203 | row.operator( 204 | AMTH_SCENE_OT_layers_render_set_individual.bl_idname, 205 | text="", 206 | icon="LAYER_ACTIVE" if n in lfr else "BLANK1").number = n 207 | 208 | 209 | def ui_layers_for_render_header(self, context): 210 | 211 | preferences = context.user_preferences.addons["amaranth"].preferences 212 | 213 | if preferences.use_layers_for_render: 214 | if context.scene.get("amth_layers_for_render"): 215 | self.layout.operator( 216 | AMTH_SCENE_OT_layers_render_view.bl_idname, 217 | text="", icon="IMGDISPLAY") 218 | 219 | # // FEATURE: Set Layers to Render 220 | 221 | 222 | def register(): 223 | bpy.utils.register_class(AMTH_SCENE_OT_layers_render_clear) 224 | bpy.utils.register_class(AMTH_SCENE_OT_layers_render_save) 225 | bpy.utils.register_class(AMTH_SCENE_OT_layers_render_set_individual) 226 | bpy.utils.register_class(AMTH_SCENE_OT_layers_render_view) 227 | bpy.types.VIEW3D_HT_header.append(ui_layers_for_render_header) 228 | bpy.types.RENDERLAYER_PT_layers.append(ui_layers_for_render) 229 | 230 | 231 | def unregister(): 232 | bpy.utils.unregister_class(AMTH_SCENE_OT_layers_render_clear) 233 | bpy.utils.unregister_class(AMTH_SCENE_OT_layers_render_save) 234 | bpy.utils.unregister_class(AMTH_SCENE_OT_layers_render_set_individual) 235 | bpy.utils.unregister_class(AMTH_SCENE_OT_layers_render_view) 236 | bpy.types.VIEW3D_HT_header.remove(ui_layers_for_render_header) 237 | bpy.types.RENDERLAYER_PT_layers.remove(ui_layers_for_render) 238 | -------------------------------------------------------------------------------- /render/render_output_z.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | EXR Render: Warn when Z not connected 16 | Display a little warning label when exporting EXR, with Z Buffer enabled, but 17 | forgot to plug the Z input in the Compositor. 18 | 19 | Might be a bit too specific, but found it nice to remember to plug the Z input 20 | if we explicitely specify for Z Buffers to be saved (because it's disabled by 21 | default). 22 | 23 | Find it on the Output panel, Render properties. 24 | """ 25 | import bpy 26 | 27 | 28 | # // FEATURE: Object ID for objects inside DupliGroups 29 | # UI: Warning about Z not connected when using EXR 30 | def ui_render_output_z(self, context): 31 | 32 | scene = bpy.context.scene 33 | image = scene.render.image_settings 34 | if scene.render.use_compositing and \ 35 | image.file_format == 'OPEN_EXR' and \ 36 | image.use_zbuffer: 37 | if scene.node_tree and scene.node_tree.nodes: 38 | for no in scene.node_tree.nodes: 39 | if no.type == 'COMPOSITE': 40 | if not no.inputs['Z'].is_linked: 41 | self.layout.label( 42 | text="The Z output in node \"%s\" is not connected" % 43 | no.name, icon="ERROR") 44 | 45 | # // UI: Warning about Z not connected 46 | 47 | 48 | def register(): 49 | bpy.types.RENDER_PT_output.append(ui_render_output_z) 50 | 51 | 52 | def unregister(): 53 | bpy.types.RENDER_PT_output.remove(ui_render_output_z) 54 | -------------------------------------------------------------------------------- /render/samples_scene.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Cycles: Samples per Scene 16 | 17 | When working in production, it's often more convenient to do lighting and 18 | compositing in different scenes (so you can later append the comp scene 19 | to bring together nodes, settings, lamps, RenderLayers). 20 | 21 | This would lead to work with more than one scene. When doing render tests 22 | you want to know at a glance how many samples the other scenes have, 23 | without manually switching. This is the idea behind the feature. 24 | 25 | Find it on the Sampling panel, on Render properties. 26 | Developed during Caminandes Open Movie Project 27 | """ 28 | 29 | import bpy 30 | from amaranth import utils 31 | from bpy.props import ( 32 | BoolProperty, 33 | IntProperty, 34 | ) 35 | 36 | 37 | class AMTH_RENDER_OT_cycles_samples_percentage_set(bpy.types.Operator): 38 | 39 | """Save the current number of samples per shader as final (gets saved in .blend)""" 40 | bl_idname = "scene.amaranth_cycles_samples_percentage_set" 41 | bl_label = "Set as Render Samples" 42 | 43 | def execute(self, context): 44 | cycles = context.scene.cycles 45 | cycles.use_samples_final = True 46 | 47 | context.scene["amth_cycles_samples_final"] = [ 48 | cycles.diffuse_samples, 49 | cycles.glossy_samples, 50 | cycles.transmission_samples, 51 | cycles.ao_samples, 52 | cycles.mesh_light_samples, 53 | cycles.subsurface_samples, 54 | cycles.volume_samples] 55 | 56 | self.report({"INFO"}, "Render Samples Saved") 57 | 58 | return {"FINISHED"} 59 | 60 | 61 | class AMTH_RENDER_OT_cycles_samples_percentage(bpy.types.Operator): 62 | 63 | """Set a percentage of the final render samples""" 64 | bl_idname = "scene.amaranth_cycles_samples_percentage" 65 | bl_label = "Set Render Samples Percentage" 66 | 67 | percent: IntProperty( 68 | name="Percentage", 69 | description="Percentage to divide render samples by", 70 | subtype="PERCENTAGE", default=0 71 | ) 72 | 73 | def execute(self, context): 74 | percent = self.percent 75 | cycles = context.scene.cycles 76 | cycles_samples_final = context.scene["amth_cycles_samples_final"] 77 | 78 | cycles.use_samples_final = False 79 | 80 | if percent == 100: 81 | cycles.use_samples_final = True 82 | 83 | cycles.diffuse_samples = int((cycles_samples_final[0] / 100) * percent) 84 | cycles.glossy_samples = int((cycles_samples_final[1] / 100) * percent) 85 | cycles.transmission_samples = int( 86 | (cycles_samples_final[2] / 100) * percent) 87 | cycles.ao_samples = int((cycles_samples_final[3] / 100) * percent) 88 | cycles.mesh_light_samples = int( 89 | (cycles_samples_final[4] / 100) * percent) 90 | cycles.subsurface_samples = int( 91 | (cycles_samples_final[5] / 100) * percent) 92 | cycles.volume_samples = int((cycles_samples_final[6] / 100) * percent) 93 | 94 | return {"FINISHED"} 95 | 96 | 97 | def render_cycles_scene_samples(self, context): 98 | 99 | layout = self.layout 100 | scene = context.scene 101 | render = scene.render 102 | if utils.cycles_exists(): 103 | cscene = scene.cycles 104 | list_sampling = scene.amaranth_cycles_list_sampling 105 | 106 | # Set Render Samples 107 | if utils.cycles_exists() and cscene.progressive == "BRANCHED_PATH": 108 | layout.separator() 109 | split = layout.split() 110 | col = split.column() 111 | 112 | col.operator( 113 | AMTH_RENDER_OT_cycles_samples_percentage_set.bl_idname, 114 | text="%s" % 115 | "Set as Render Samples" if cscene.use_samples_final else "Set New Render Samples", 116 | icon="%s" % 117 | "PINNED" if cscene.use_samples_final else "UNPINNED") 118 | 119 | col = split.column() 120 | row = col.row(align=True) 121 | row.enabled = True if scene.get("amth_cycles_samples_final") else False 122 | 123 | row.operator( 124 | AMTH_RENDER_OT_cycles_samples_percentage.bl_idname, 125 | text="100%").percent = 100 126 | row.operator( 127 | AMTH_RENDER_OT_cycles_samples_percentage.bl_idname, 128 | text="75%").percent = 75 129 | row.operator( 130 | AMTH_RENDER_OT_cycles_samples_percentage.bl_idname, 131 | text="50%").percent = 50 132 | row.operator( 133 | AMTH_RENDER_OT_cycles_samples_percentage.bl_idname, 134 | text="25%").percent = 25 135 | 136 | # List Samples 137 | #if (len(scene.render.layers) > 1) or (len(bpy.data.scenes) > 1): 138 | if (len(scene.render.views) > 1) or (len(bpy.data.scenes) > 1): 139 | 140 | box = layout.box() 141 | row = box.row(align=True) 142 | col = row.column(align=True) 143 | 144 | row = col.row(align=True) 145 | row.alignment = "LEFT" 146 | row.prop(scene, "amaranth_cycles_list_sampling", 147 | icon="%s" % "TRIA_DOWN" if list_sampling else "TRIA_RIGHT", 148 | emboss=False) 149 | 150 | if list_sampling: 151 | #if len(scene.render.layers) == 1 and render.layers[0].samples == 0: 152 | if len(scene.render.views) == 1 and render.view_layers[0].samples == 0: 153 | pass 154 | else: 155 | col.separator() 156 | #col.label(text="RenderLayers:", icon="RENDERLAYERS") 157 | col.label(text="View Layers:", icon="RENDERLAYERS") 158 | 159 | #for rl in scene.render.layers: 160 | for rl in scene.view_layers: 161 | row = col.row(align=True) 162 | row.label(text=rl.name, icon="BLANK1") 163 | row.prop( 164 | rl, "samples", text="%s" % 165 | "Samples" if rl.samples > 0 else "Automatic (%s)" % 166 | (cscene.aa_samples if cscene.progressive == "BRANCHED_PATH" else cscene.samples)) 167 | 168 | if (len(bpy.data.scenes) > 1): 169 | col.separator() 170 | 171 | col.label(text="Scenes:", icon="SCENE_DATA") 172 | 173 | if utils.cycles_exists() and cscene.progressive == "PATH": 174 | for s in bpy.data.scenes: 175 | if s != scene: 176 | row = col.row(align=True) 177 | if s.render.engine == "CYCLES": 178 | cscene = s.cycles 179 | 180 | #row.label(s.name) 181 | row.label(text=s.name) 182 | row.prop(cscene, "samples", icon="BLANK1") 183 | else: 184 | row.label( 185 | text="Scene: '%s' is not using Cycles" % 186 | s.name) 187 | else: 188 | for s in bpy.data.scenes: 189 | if s != scene: 190 | row = col.row(align=True) 191 | if s.render.engine == "CYCLES": 192 | cscene = s.cycles 193 | 194 | row.label(text=s.name, icon="BLANK1") 195 | row.prop(cscene, "aa_samples", 196 | text="AA Samples") 197 | else: 198 | row.label( 199 | text="Scene: '%s' is not using Cycles" % 200 | s.name) 201 | 202 | 203 | def init(): 204 | scene = bpy.types.Scene 205 | if utils.cycles_exists(): 206 | scene.amaranth_cycles_list_sampling = bpy.props.BoolProperty( 207 | default=False, 208 | name="Samples Per:") 209 | # Note: add versioning code to adress changes introduced in 2.79.1 210 | if bpy.app.version >= (2, 79, 1): 211 | from cycles import properties as _cycles_props 212 | _cycles_props.CyclesRenderSettings.use_samples_final = BoolProperty( 213 | name="Use Final Render Samples", 214 | description="Use current shader samples as final render samples", 215 | default=False 216 | ) 217 | else: 218 | bpy.types.CyclesRenderSettings.use_samples_final = BoolProperty( 219 | name="Use Final Render Samples", 220 | description="Use current shader samples as final render samples", 221 | default=False 222 | ) 223 | 224 | 225 | 226 | def clear(): 227 | wm = bpy.context.window_manager 228 | for p in ("amarath_cycles_list_sampling", "use_samples_final"): 229 | if p in wm: 230 | del wm[p] 231 | 232 | 233 | def register(): 234 | init() 235 | bpy.utils.register_class(AMTH_RENDER_OT_cycles_samples_percentage) 236 | bpy.utils.register_class(AMTH_RENDER_OT_cycles_samples_percentage_set) 237 | if utils.cycles_exists(): 238 | if bpy.app.version >= (2, 79, 1): 239 | bpy.types.CYCLES_RENDER_PT_sampling.append(render_cycles_scene_samples) 240 | else: 241 | bpy.types.CyclesRender_PT_sampling.append(render_cycles_scene_samples) 242 | 243 | 244 | def unregister(): 245 | bpy.utils.unregister_class(AMTH_RENDER_OT_cycles_samples_percentage) 246 | bpy.utils.unregister_class(AMTH_RENDER_OT_cycles_samples_percentage_set) 247 | if utils.cycles_exists(): 248 | if bpy.app.version >= (2, 79, 1): 249 | bpy.types.CYCLES_RENDER_PT_sampling.remove(render_cycles_scene_samples) 250 | else: 251 | bpy.types.CyclesRender_PT_sampling.remove(render_cycles_scene_samples) 252 | 253 | 254 | clear() 255 | -------------------------------------------------------------------------------- /render/unsimplify.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Unsimplify Render 16 | 17 | Handy option when you want to simplify the 3D View but unsimplify during 18 | render. Find it on the Simplify panel under Scene properties. 19 | """ 20 | 21 | import bpy 22 | from amaranth import utils 23 | 24 | 25 | def init(): 26 | scene = bpy.types.Scene 27 | scene.use_unsimplify_render = bpy.props.BoolProperty( 28 | default=False, 29 | name="Unsimplify Render", 30 | description="Disable Simplify during render") 31 | scene.simplify_status = bpy.props.BoolProperty(default=False) 32 | 33 | 34 | def clear(): 35 | wm = bpy.context.window_manager 36 | for p in ("use_unsimplify_render", "simplify_status"): 37 | if wm.get(p): 38 | del wm[p] 39 | 40 | 41 | @bpy.app.handlers.persistent 42 | def unsimplify_render_pre(scene): 43 | render = scene.render 44 | scene.simplify_status = render.use_simplify 45 | 46 | if scene.use_unsimplify_render: 47 | render.use_simplify = False 48 | 49 | 50 | @bpy.app.handlers.persistent 51 | def unsimplify_render_post(scene): 52 | render = scene.render 53 | render.use_simplify = scene.simplify_status 54 | 55 | 56 | def unsimplify_ui(self, context): 57 | scene = bpy.context.scene 58 | self.layout.prop(scene, 'use_unsimplify_render') 59 | 60 | 61 | def register(): 62 | init() 63 | bpy.app.handlers.render_pre.append(unsimplify_render_pre) 64 | bpy.app.handlers.render_post.append(unsimplify_render_post) 65 | bpy.types.SCENE_PT_simplify.append(unsimplify_ui) 66 | if utils.cycles_exists(): 67 | bpy.types.CyclesScene_PT_simplify.append(unsimplify_ui) 68 | 69 | 70 | def unregister(): 71 | clear() 72 | bpy.app.handlers.render_pre.remove(unsimplify_render_pre) 73 | bpy.app.handlers.render_post.remove(unsimplify_render_post) 74 | bpy.types.SCENE_PT_simplify.remove(unsimplify_ui) 75 | if utils.cycles_exists(): 76 | bpy.types.CyclesScene_PT_simplify.remove(unsimplify_ui) 77 | -------------------------------------------------------------------------------- /scene/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/venomgfx/amaranth/e2a34ae7fb86f66ac931a40cdc9d20d0f2ebb583/scene/__init__.py -------------------------------------------------------------------------------- /scene/current_blend.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | File Browser > Go to Current Blend's Folder 16 | 17 | For when you're lost browsing files and want to go back to the currently 18 | open blend's directory. Look for it on the File Browser's header, only 19 | shows up if the file is saved. 20 | """ 21 | 22 | import bpy 23 | 24 | # From space_filebrowser.py 25 | def panel_poll_is_upper_region(region): 26 | # The upper region is left-aligned, the lower is split into it then. 27 | # Note that after "Flip Regions" it's right-aligned. 28 | return region.alignment in {'LEFT', 'RIGHT'} 29 | 30 | 31 | class AMTH_FILE_OT_directory_current_blend(bpy.types.Operator): 32 | 33 | """Go to the directory of the currently open blend file""" 34 | bl_idname = "file.directory_current_blend" 35 | bl_label = "Current Blend's Folder" 36 | 37 | def execute(self, context): 38 | bpy.ops.file.select_bookmark(dir="//") 39 | return {"FINISHED"} 40 | 41 | 42 | class FILEBROWSER_PT_amaranth(bpy.types.Panel): 43 | bl_space_type = 'FILE_BROWSER' 44 | bl_region_type = 'TOOLS' 45 | bl_category = "Bookmarks" 46 | bl_label = "Amaranth" 47 | bl_options = {'HIDE_HEADER'} 48 | 49 | @classmethod 50 | def poll(cls, context): 51 | return panel_poll_is_upper_region(context.region) 52 | 53 | def draw(self, context): 54 | layout = self.layout 55 | layout.scale_x = 1.3 56 | layout.scale_y = 1.3 57 | 58 | if bpy.data.filepath: 59 | row = layout.row() 60 | flow = row.grid_flow(row_major=False, columns=0, even_columns=False, even_rows=False, align=True) 61 | 62 | subrow = flow.row() 63 | subsubrow = subrow.row(align=True) 64 | subsubrow.operator( 65 | AMTH_FILE_OT_directory_current_blend.bl_idname, 66 | icon="DESKTOP") 67 | 68 | 69 | classes = ( 70 | AMTH_FILE_OT_directory_current_blend, 71 | FILEBROWSER_PT_amaranth 72 | ) 73 | 74 | def register(): 75 | for cls in classes: 76 | bpy.utils.register_class(cls) 77 | 78 | def unregister(): 79 | for cls in classes: 80 | bpy.utils.unregister_class(cls) 81 | -------------------------------------------------------------------------------- /scene/debug.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Scene Debug Panel 16 | 17 | This is something I've been wanting to have for a while, a way to know 18 | certain info about your scene. A way to "debug" it, especially when 19 | working in production with other teams, this came in very handy. 20 | 21 | Being mostly a lighting guy myself, I needed two main features to start with: 22 | 23 | * List Cycles Material using X shader 24 | Where X is any shader type you want. It will display (and print on console) 25 | a list of all the materials containing the shader you specified above. 26 | Good for finding out if there's any Meshlight (Emission) material hidden, 27 | or if there are many glossy shaders making things noisy. 28 | A current limitation is that it doesn't look inside node groups (yet, 29 | working on it!). It works since 0.8.8! 30 | 31 | Under the "Scene Debug" panel in Scene properties. 32 | 33 | * Lighter's Corner 34 | This is an UI List of Lights in the scene(s). 35 | It allows you to quickly see how many lights you have, select them by 36 | clicking on their name, see their type (icon), samples number (if using 37 | Branched Path Tracing), size, and change their visibility. 38 | 39 | """ 40 | 41 | # TODO: module cleanup! maybe break it up in a package 42 | # dicts instead of if, elif, else all over the place. 43 | # helper functions instead of everything on the execute method. 44 | # str.format() + dicts instead of inline % op all over the place. 45 | # remove/manage debug print calls. 46 | # avoid duplicate code/patterns through helper functions. 47 | 48 | import os 49 | import bpy 50 | from amaranth import utils 51 | from bpy.types import ( 52 | Operator, 53 | Panel, 54 | UIList, 55 | PropertyGroup, 56 | ) 57 | from bpy.props import ( 58 | BoolProperty, 59 | CollectionProperty, 60 | EnumProperty, 61 | IntProperty, 62 | PointerProperty, 63 | StringProperty, 64 | ) 65 | 66 | # default string used in the List Users for Datablock section menus 67 | USER_X_NAME_EMPTY = "Data Block not selected/existing" 68 | 69 | 70 | class AMTH_store_data(): 71 | # used by: AMTH_SCENE_OT_list_users_for_x operator 72 | users = { 73 | 'OBJECT_DATA': [], # Store Objects with Material 74 | 'MATERIAL': [], # Materials (Node tree) 75 | 'LIGHT': [], # Lights 76 | 'WORLD': [], # World 77 | 'TEXTURE': [], # Textures (Psys, Brushes) 78 | 'MODIFIER': [], # Modifiers 79 | 'MESH_DATA': [], # Vertex Colors 80 | 'VIEW3D': [], # Background Images 81 | 'NODETREE': [], # Compositor 82 | } 83 | libraries = [] # Libraries x type 84 | 85 | # used by: AMTH_SCENE_OT_list_missing_material_slots operator 86 | obj_mat_slots = [] # Missing material slots 87 | obj_mat_slots_lib = [] # Libraries with missing material slots 88 | 89 | # used by: AMTH_SCENE_OT_cycles_shader_list_nodes operator 90 | mat_shaders = [] # Materials that use a specific shader 91 | 92 | # used by : AMTH_SCENE_OT_list_missing_node_links operator 93 | count_groups = 0 # Missing node groups count 94 | count_images = 0 # Missing node images 95 | count_image_node_unlinked = 0 # Unlinked Image nodes 96 | 97 | 98 | def call_update_datablock_type(self, context): 99 | try: 100 | # Note: this is pretty weak, but updates the operator enum selection 101 | bpy.ops.scene.amth_list_users_for_x_type(list_type_select='0') 102 | except: 103 | pass 104 | 105 | 106 | def init(): 107 | scene = bpy.types.Scene 108 | 109 | scene.amaranth_lighterscorner_list_meshlights = BoolProperty( 110 | default=False, 111 | name="List Meshlights", 112 | description="Include light emitting meshes on the list" 113 | ) 114 | amth_datablock_types = ( 115 | ("IMAGE_DATA", "Image", "Image Datablocks", 0), 116 | ("MATERIAL", "Material", "Material Datablocks", 1), 117 | ("GROUP_VCOL", "Vertex Colors", "Vertex Color Layers", 2), 118 | ) 119 | scene.amth_datablock_types = EnumProperty( 120 | items=amth_datablock_types, 121 | name="Type", 122 | description="Datablock Type", 123 | default="MATERIAL", 124 | update=call_update_datablock_type, 125 | options={"SKIP_SAVE"} 126 | ) 127 | if utils.cycles_exists(): 128 | cycles_shader_node_types = ( 129 | ("BSDF_DIFFUSE", "Diffuse BSDF", "", 0), 130 | ("BSDF_GLOSSY", "Glossy BSDF", "", 1), 131 | ("BSDF_TRANSPARENT", "Transparent BSDF", "", 2), 132 | ("BSDF_REFRACTION", "Refraction BSDF", "", 3), 133 | ("BSDF_GLASS", "Glass BSDF", "", 4), 134 | ("BSDF_TRANSLUCENT", "Translucent BSDF", "", 5), 135 | ("BSDF_ANISOTROPIC", "Anisotropic BSDF", "", 6), 136 | ("BSDF_VELVET", "Velvet BSDF", "", 7), 137 | ("BSDF_TOON", "Toon BSDF", "", 8), 138 | ("SUBSURFACE_SCATTERING", "Subsurface Scattering", "", 9), 139 | ("EMISSION", "Emission", "", 10), 140 | ("BSDF_HAIR", "Hair BSDF", "", 11), 141 | ("BACKGROUND", "Background", "", 12), 142 | ("AMBIENT_OCCLUSION", "Ambient Occlusion", "", 13), 143 | ("HOLDOUT", "Holdout", "", 14), 144 | ("VOLUME_ABSORPTION", "Volume Absorption", "", 15), 145 | ("VOLUME_SCATTER", "Volume Scatter", "", 16), 146 | ("MIX_SHADER", "Mix Shader", "", 17), 147 | ("ADD_SHADER", "Add Shader", "", 18), 148 | ('BSDF_PRINCIPLED', 'Principled BSDF', "", 19), 149 | ) 150 | scene.amaranth_cycles_node_types = EnumProperty( 151 | items=cycles_shader_node_types, 152 | name="Shader" 153 | ) 154 | 155 | 156 | def clear(): 157 | props = ( 158 | "amaranth_cycles_node_types", 159 | "amaranth_lighterscorner_list_meshlights", 160 | ) 161 | wm = bpy.context.window_manager 162 | for p in props: 163 | if wm.get(p): 164 | del wm[p] 165 | 166 | 167 | def print_with_count_list(text="", send_list=[]): 168 | if text: 169 | print("\n* {}\n".format(text)) 170 | if not send_list: 171 | print("List is empty, no items to display") 172 | return 173 | 174 | for i, entry in enumerate(send_list): 175 | print('{:02d}. {}'.format(i + 1, send_list[i])) 176 | print("\n") 177 | 178 | 179 | def print_grammar(line="", single="", multi="", cond=[]): 180 | phrase = single if len(cond) == 1 else multi 181 | print("\n* {} {}:\n".format(line, phrase)) 182 | 183 | 184 | def reset_global_storage(what="NONE"): 185 | if what == "NONE": 186 | return 187 | 188 | if what == "XTYPE": 189 | for user in AMTH_store_data.users: 190 | AMTH_store_data.users[user] = [] 191 | AMTH_store_data.libraries = [] 192 | 193 | elif what == "MAT_SLOTS": 194 | AMTH_store_data.obj_mat_slots[:] = [] 195 | AMTH_store_data.obj_mat_slots_lib[:] = [] 196 | 197 | elif what == "NODE_LINK": 198 | AMTH_store_data.obj_mat_slots[:] = [] 199 | AMTH_store_data.count_groups = 0 200 | AMTH_store_data.count_images = 0 201 | AMTH_store_data.count_image_node_unlinked = 0 202 | 203 | elif what == "SHADER": 204 | AMTH_store_data.mat_shaders[:] = [] 205 | 206 | 207 | class AMTH_SCENE_OT_cycles_shader_list_nodes(Operator): 208 | """List Cycles materials containing a specific shader""" 209 | bl_idname = "scene.cycles_list_nodes" 210 | bl_label = "List Materials" 211 | 212 | @classmethod 213 | def poll(cls, context): 214 | return utils.cycles_exists() and utils.cycles_active(context) 215 | 216 | def execute(self, context): 217 | node_type = context.scene.amaranth_cycles_node_types 218 | roughness = False 219 | shaders_roughness = ("BSDF_GLOSSY", "BSDF_DIFFUSE", "BSDF_GLASS") 220 | 221 | reset_global_storage("SHADER") 222 | 223 | print("\n=== Cycles Shader Type: {} === \n".format(node_type)) 224 | 225 | for ma in bpy.data.materials: 226 | if not ma.node_tree: 227 | continue 228 | 229 | nodes = ma.node_tree.nodes 230 | print_unconnected = ( 231 | "Note: \nOutput from \"{}\" node in material \"{}\" " 232 | "not connected\n".format(node_type, ma.name) 233 | ) 234 | 235 | for no in nodes: 236 | if no.type == node_type: 237 | for ou in no.outputs: 238 | if ou.links: 239 | connected = True 240 | if no.type in shaders_roughness: 241 | roughness = "R: {:.4f}".format( 242 | no.inputs["Roughness"].default_value 243 | ) 244 | else: 245 | roughness = False 246 | else: 247 | connected = False 248 | print(print_unconnected) 249 | 250 | if ma.name not in AMTH_store_data.mat_shaders: 251 | AMTH_store_data.mat_shaders.append( 252 | "%s%s [%s] %s%s%s" % 253 | ("[L] " if ma.library else "", 254 | ma.name, 255 | ma.users, 256 | "[F]" if ma.use_fake_user else "", 257 | " - [%s]" % 258 | roughness if roughness else "", 259 | " * Output not connected" if not connected else "") 260 | ) 261 | elif no.type == "GROUP": 262 | if no.node_tree: 263 | for nog in no.node_tree.nodes: 264 | if nog.type == node_type: 265 | for ou in nog.outputs: 266 | if ou.links: 267 | connected = True 268 | if nog.type in shaders_roughness: 269 | roughness = "R: {:.4f}".format( 270 | nog.inputs["Roughness"].default_value 271 | ) 272 | else: 273 | roughness = False 274 | else: 275 | connected = False 276 | print(print_unconnected) 277 | 278 | if ma.name not in AMTH_store_data.mat_shaders: 279 | AMTH_store_data.mat_shaders.append( 280 | '%s%s%s [%s] %s%s%s' % 281 | ("[L] " if ma.library else "", 282 | "Node Group: %s%s -> " % 283 | ("[L] " if no.node_tree.library else "", 284 | no.node_tree.name), 285 | ma.name, 286 | ma.users, 287 | "[F]" if ma.use_fake_user else "", 288 | " - [%s]" % 289 | roughness if roughness else "", 290 | " * Output not connected" if not connected else "") 291 | ) 292 | AMTH_store_data.mat_shaders = sorted(list(set(AMTH_store_data.mat_shaders))) 293 | 294 | message = "No materials with nodes type {} found".format(node_type) 295 | if len(AMTH_store_data.mat_shaders) > 0: 296 | message = "A total of {} {} using {} found".format( 297 | len(AMTH_store_data.mat_shaders), 298 | "material" if len(AMTH_store_data.mat_shaders) == 1 else "materials", 299 | node_type) 300 | print_with_count_list(send_list=AMTH_store_data.mat_shaders) 301 | 302 | self.report({'INFO'}, message) 303 | AMTH_store_data.mat_shaders = sorted(list(set(AMTH_store_data.mat_shaders))) 304 | 305 | return {"FINISHED"} 306 | 307 | 308 | class AMTH_SCENE_OT_amaranth_object_select(Operator): 309 | """Select object""" 310 | bl_idname = "scene.amaranth_object_select" 311 | bl_label = "Select Object" 312 | 313 | object_name: StringProperty() 314 | 315 | def execute(self, context): 316 | if not (self.object_name and self.object_name in bpy.data.objects): 317 | self.report({'WARNING'}, 318 | "Object with the given name could not be found. Operation Cancelled") 319 | return {"CANCELLED"} 320 | 321 | obj = bpy.data.objects[self.object_name] 322 | 323 | bpy.ops.object.select_all(action="DESELECT") 324 | obj.select_set(True) 325 | context.view_layer.objects.active = obj 326 | 327 | return {"FINISHED"} 328 | 329 | 330 | class AMTH_SCENE_OT_list_missing_node_links(Operator): 331 | """Print a list of missing node links""" 332 | bl_idname = "scene.list_missing_node_links" 333 | bl_label = "List Missing Node Links" 334 | 335 | def execute(self, context): 336 | missing_groups = [] 337 | missing_images = [] 338 | image_nodes_unlinked = [] 339 | libraries = [] 340 | 341 | reset_global_storage(what="NODE_LINK") 342 | 343 | for ma in bpy.data.materials: 344 | if not ma.node_tree: 345 | continue 346 | 347 | for no in ma.node_tree.nodes: 348 | if no.type == "GROUP": 349 | if not no.node_tree: 350 | AMTH_store_data.count_groups += 1 351 | 352 | users_ngroup = [] 353 | 354 | for ob in bpy.data.objects: 355 | if ob.material_slots and ma.name in ob.material_slots: 356 | users_ngroup.append("%s%s%s" % ( 357 | "[L] " if ob.library else "", 358 | "[F] " if ob.use_fake_user else "", 359 | ob.name)) 360 | 361 | missing_groups.append( 362 | "MA: %s%s%s [%s]%s%s%s\n" % 363 | ("[L] " if ma.library else "", 364 | "[F] " if ma.use_fake_user else "", 365 | ma.name, 366 | ma.users, 367 | " *** No users *** " if ma.users == 0 else "", 368 | "\nLI: %s" % 369 | ma.library.filepath if ma.library else "", 370 | "\nOB: %s" % 371 | ", ".join(users_ngroup) if users_ngroup else "") 372 | ) 373 | if ma.library: 374 | libraries.append(ma.library.filepath) 375 | 376 | if no.type == "TEX_IMAGE": 377 | 378 | outputs_empty = not no.outputs["Color"].is_linked and \ 379 | not no.outputs["Alpha"].is_linked 380 | 381 | if no.image: 382 | image_path_exists = os.path.exists( 383 | bpy.path.abspath( 384 | no.image.filepath, 385 | library=no.image.library) 386 | ) 387 | 388 | if outputs_empty or not no.image or not image_path_exists: 389 | 390 | users_images = [] 391 | 392 | for ob in bpy.data.objects: 393 | if ob.material_slots and ma.name in ob.material_slots: 394 | users_images.append("%s%s%s" % ( 395 | "[L] " if ob.library else "", 396 | "[F] " if ob.use_fake_user else "", 397 | ob.name)) 398 | 399 | if outputs_empty: 400 | AMTH_store_data.count_image_node_unlinked += 1 401 | 402 | image_nodes_unlinked.append( 403 | "%s%s%s%s%s [%s]%s%s%s%s%s\n" % 404 | ("NO: %s" % 405 | no.name, 406 | "\nMA: ", 407 | "[L] " if ma.library else "", 408 | "[F] " if ma.use_fake_user else "", 409 | ma.name, 410 | ma.users, 411 | " *** No users *** " if ma.users == 0 else "", 412 | "\nLI: %s" % 413 | ma.library.filepath if ma.library else "", 414 | "\nIM: %s" % 415 | no.image.name if no.image else "", 416 | "\nLI: %s" % 417 | no.image.filepath if no.image and no.image.filepath else "", 418 | "\nOB: %s" % 419 | ', '.join(users_images) if users_images else "")) 420 | 421 | if not no.image or not image_path_exists: 422 | AMTH_store_data.count_images += 1 423 | 424 | missing_images.append( 425 | "MA: %s%s%s [%s]%s%s%s%s%s\n" % 426 | ("[L] " if ma.library else "", 427 | "[F] " if ma.use_fake_user else "", 428 | ma.name, 429 | ma.users, 430 | " *** No users *** " if ma.users == 0 else "", 431 | "\nLI: %s" % 432 | ma.library.filepath if ma.library else "", 433 | "\nIM: %s" % 434 | no.image.name if no.image else "", 435 | "\nLI: %s" % 436 | no.image.filepath if no.image and no.image.filepath else "", 437 | "\nOB: %s" % 438 | ', '.join(users_images) if users_images else "")) 439 | 440 | if ma.library: 441 | libraries.append(ma.library.filepath) 442 | 443 | # Remove duplicates and sort 444 | missing_groups = sorted(list(set(missing_groups))) 445 | missing_images = sorted(list(set(missing_images))) 446 | image_nodes_unlinked = sorted(list(set(image_nodes_unlinked))) 447 | libraries = sorted(list(set(libraries))) 448 | 449 | print( 450 | "\n\n== %s missing image %s, %s missing node %s and %s image %s unlinked ==" % 451 | ("No" if AMTH_store_data.count_images == 0 else str( 452 | AMTH_store_data.count_images), 453 | "node" if AMTH_store_data.count_images == 1 else "nodes", 454 | "no" if AMTH_store_data.count_groups == 0 else str( 455 | AMTH_store_data.count_groups), 456 | "group" if AMTH_store_data.count_groups == 1 else "groups", 457 | "no" if AMTH_store_data.count_image_node_unlinked == 0 else str( 458 | AMTH_store_data.count_image_node_unlinked), 459 | "node" if AMTH_store_data.count_groups == 1 else "nodes") 460 | ) 461 | # List Missing Node Groups 462 | if missing_groups: 463 | print_with_count_list("Missing Node Group Links", missing_groups) 464 | 465 | # List Missing Image Nodes 466 | if missing_images: 467 | print_with_count_list("Missing Image Nodes Link", missing_images) 468 | 469 | # List Image Nodes with its outputs unlinked 470 | if image_nodes_unlinked: 471 | print_with_count_list("Image Nodes Unlinked", image_nodes_unlinked) 472 | 473 | if missing_groups or missing_images or image_nodes_unlinked: 474 | if libraries: 475 | print_grammar("That's bad, run check", "this library", "these libraries", libraries) 476 | print_with_count_list(send_list=libraries) 477 | else: 478 | self.report({"INFO"}, "Yay! No missing node links") 479 | 480 | if missing_groups and missing_images: 481 | self.report( 482 | {"WARNING"}, 483 | "%d missing image %s and %d missing node %s found" % 484 | (AMTH_store_data.count_images, 485 | "node" if AMTH_store_data.count_images == 1 else "nodes", 486 | AMTH_store_data.count_groups, 487 | "group" if AMTH_store_data.count_groups == 1 else "groups") 488 | ) 489 | 490 | return {"FINISHED"} 491 | 492 | 493 | class AMTH_SCENE_OT_list_missing_material_slots(Operator): 494 | """List objects with empty material slots""" 495 | bl_idname = "scene.list_missing_material_slots" 496 | bl_label = "List Empty Material Slots" 497 | 498 | def execute(self, context): 499 | reset_global_storage("MAT_SLOTS") 500 | 501 | for ob in bpy.data.objects: 502 | for ma in ob.material_slots: 503 | if not ma.material: 504 | AMTH_store_data.obj_mat_slots.append('{}{}'.format( 505 | '[L] ' if ob.library else '', ob.name)) 506 | if ob.library: 507 | AMTH_store_data.obj_mat_slots_lib.append(ob.library.filepath) 508 | 509 | AMTH_store_data.obj_mat_slots = sorted(list(set(AMTH_store_data.obj_mat_slots))) 510 | AMTH_store_data.obj_mat_slots_lib = sorted(list(set(AMTH_store_data.obj_mat_slots_lib))) 511 | 512 | if len(AMTH_store_data.obj_mat_slots) == 0: 513 | self.report({"INFO"}, 514 | "No objects with empty material slots found") 515 | return {"FINISHED"} 516 | 517 | print( 518 | "\n* A total of {} {} with empty material slots was found \n".format( 519 | len(AMTH_store_data.obj_mat_slots), 520 | "object" if len(AMTH_store_data.obj_mat_slots) == 1 else "objects") 521 | ) 522 | print_with_count_list(send_list=AMTH_store_data.obj_mat_slots) 523 | 524 | if AMTH_store_data.obj_mat_slots_lib: 525 | print_grammar("Check", "this library", "these libraries", 526 | AMTH_store_data.obj_mat_slots_lib 527 | ) 528 | print_with_count_list(send_list=AMTH_store_data.obj_mat_slots_lib) 529 | 530 | return {"FINISHED"} 531 | 532 | 533 | class AMTH_SCENE_OT_list_users_for_x_type(Operator): 534 | bl_idname = "scene.amth_list_users_for_x_type" 535 | bl_label = "Select" 536 | bl_description = "Select Datablock Name" 537 | 538 | @staticmethod 539 | def fill_where(): 540 | where = [] 541 | data_block = bpy.context.scene.amth_datablock_types 542 | 543 | if data_block == 'IMAGE_DATA': 544 | for im in bpy.data.images: 545 | if im.name not in {'Render Result', 'Viewer Node'}: 546 | where.append(im) 547 | 548 | elif data_block == 'MATERIAL': 549 | where = bpy.data.materials 550 | 551 | elif data_block == 'GROUP_VCOL': 552 | for ob in bpy.data.objects: 553 | if ob.type == 'MESH': 554 | for v in ob.data.vertex_colors: 555 | if v and v not in where: 556 | where.append(v) 557 | where = list(set(where)) 558 | 559 | return where 560 | 561 | def avail(self, context): 562 | datablock_type = bpy.context.scene.amth_datablock_types 563 | where = AMTH_SCENE_OT_list_users_for_x_type.fill_where() 564 | items = [(str(i), x.name, x.name, datablock_type, i) for i, x in enumerate(where)] 565 | items = sorted(list(set(items))) 566 | if not items: 567 | items = [('0', USER_X_NAME_EMPTY, USER_X_NAME_EMPTY, "INFO", 0)] 568 | return items 569 | 570 | list_type_select: EnumProperty( 571 | items=avail, 572 | name="Available", 573 | options={"SKIP_SAVE"} 574 | ) 575 | 576 | @classmethod 577 | def poll(cls, context): 578 | return bpy.context.scene.amth_datablock_types 579 | 580 | def execute(self, context): 581 | where = self.fill_where() 582 | bpy.context.scene.amth_list_users_for_x_name = \ 583 | where[int(self.list_type_select)].name if where else USER_X_NAME_EMPTY 584 | 585 | return {'FINISHED'} 586 | 587 | 588 | class AMTH_SCENE_OT_list_users_for_x(Operator): 589 | """List users for a particular datablock""" 590 | bl_idname = "scene.amth_list_users_for_x" 591 | bl_label = "List Users for Datablock" 592 | 593 | name: StringProperty() 594 | 595 | def execute(self, context): 596 | d = bpy.data 597 | x = self.name if self.name else context.scene.amth_list_users_for_x_name 598 | 599 | if USER_X_NAME_EMPTY in x: 600 | self.report({'INFO'}, 601 | "Please select a DataBlock name first. Operation Cancelled") 602 | return {"CANCELLED"} 603 | 604 | dtype = context.scene.amth_datablock_types 605 | 606 | reset_global_storage("XTYPE") 607 | 608 | # IMAGE TYPE 609 | if dtype == 'IMAGE_DATA': 610 | # Check Materials 611 | for ma in d.materials: 612 | # Cycles 613 | if utils.cycles_exists(): 614 | if ma and ma.node_tree and ma.node_tree.nodes: 615 | materials = [] 616 | 617 | for nd in ma.node_tree.nodes: 618 | if nd and nd.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}: 619 | materials.append(nd) 620 | 621 | if nd and nd.type == 'GROUP': 622 | if nd.node_tree and nd.node_tree.nodes: 623 | for ng in nd.node_tree.nodes: 624 | if ng.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}: 625 | materials.append(ng) 626 | 627 | for no in materials: 628 | if no.image and no.image.name == x: 629 | objects = [] 630 | 631 | for ob in d.objects: 632 | if ma.name in ob.material_slots: 633 | objects.append(ob.name) 634 | links = False 635 | 636 | for o in no.outputs: 637 | if o.links: 638 | links = True 639 | 640 | name = '"{0}" {1}{2}'.format( 641 | ma.name, 642 | 'in object: {0}'.format(objects) if objects else ' (unassigned)', 643 | '' if links else ' (unconnected)') 644 | 645 | if name not in AMTH_store_data.users['MATERIAL']: 646 | AMTH_store_data.users['MATERIAL'].append(name) 647 | # Check Lights 648 | for la in d.lights: 649 | # Cycles 650 | if utils.cycles_exists(): 651 | if la and la.node_tree and la.node_tree.nodes: 652 | for no in la.node_tree.nodes: 653 | if no and \ 654 | no.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \ 655 | no.image and no.image.name == x: 656 | if la.name not in AMTH_store_data.users['LIGHT']: 657 | AMTH_store_data.users['LIGHT'].append(la.name) 658 | # Check World 659 | for wo in d.worlds: 660 | # Cycles 661 | if utils.cycles_exists(): 662 | if wo and wo.node_tree and wo.node_tree.nodes: 663 | for no in wo.node_tree.nodes: 664 | if no and \ 665 | no.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \ 666 | no.image and no.image.name == x: 667 | if wo.name not in AMTH_store_data.users['WORLD']: 668 | AMTH_store_data.users['WORLD'].append(wo.name) 669 | # Check Textures 670 | for te in d.textures: 671 | if te and te.type == 'IMAGE' and te.image: 672 | name = te.image.name 673 | 674 | if name == x and \ 675 | name not in AMTH_store_data.users['TEXTURE']: 676 | AMTH_store_data.users['TEXTURE'].append(te.name) 677 | # Check Modifiers in Objects 678 | for ob in d.objects: 679 | for mo in ob.modifiers: 680 | if mo.type in {'UV_PROJECT'}: 681 | image = mo.image 682 | 683 | if mo and image and image.name == x: 684 | name = '"{0}" modifier in {1}'.format(mo.name, ob.name) 685 | if name not in AMTH_store_data.users['MODIFIER']: 686 | AMTH_store_data.users['MODIFIER'].append(name) 687 | # Check Background Images in Viewports 688 | for scr in d.screens: 689 | for ar in scr.areas: 690 | if ar.type == 'VIEW_3D': 691 | if ar.spaces and \ 692 | ar.spaces.active and \ 693 | ar.spaces.active.background_images: 694 | for bg in ar.spaces.active.background_images: 695 | image = bg.image 696 | 697 | if bg and image and image.name == x: 698 | name = 'Background for 3D Viewport in Screen "{0}"'\ 699 | .format(scr.name) 700 | if name not in AMTH_store_data.users['VIEW3D']: 701 | AMTH_store_data.users['VIEW3D'].append(name) 702 | # Check the Compositor 703 | for sce in d.scenes: 704 | if sce.node_tree and sce.node_tree.nodes: 705 | nodes = [] 706 | for nd in sce.node_tree.nodes: 707 | if nd.type == 'IMAGE': 708 | nodes.append(nd) 709 | elif nd.type == 'GROUP': 710 | if nd.node_tree and nd.node_tree.nodes: 711 | for ng in nd.node_tree.nodes: 712 | if ng.type == 'IMAGE': 713 | nodes.append(ng) 714 | 715 | for no in nodes: 716 | if no.image and no.image.name == x: 717 | links = False 718 | 719 | for o in no.outputs: 720 | if o.links: 721 | links = True 722 | 723 | name = 'Node {0} in Compositor (Scene "{1}"){2}'.format( 724 | no.name, 725 | sce.name, 726 | '' if links else ' (unconnected)') 727 | 728 | if name not in AMTH_store_data.users['NODETREE']: 729 | AMTH_store_data.users['NODETREE'].append(name) 730 | # MATERIAL TYPE 731 | if dtype == 'MATERIAL': 732 | # Check Materials - Note: build an object_check list as only strings are stored 733 | object_check = [d.objects[names] for names in AMTH_store_data.users['OBJECT_DATA'] if 734 | names in d.objects] 735 | for ob in d.objects: 736 | for ma in ob.material_slots: 737 | if ma.name == x: 738 | if ma not in object_check: 739 | AMTH_store_data.users['OBJECT_DATA'].append(ob.name) 740 | 741 | if ob.library: 742 | AMTH_store_data.libraries.append(ob.library.filepath) 743 | # VERTEX COLOR TYPE 744 | elif dtype == 'GROUP_VCOL': 745 | # Check VCOL in Meshes 746 | for ob in bpy.data.objects: 747 | if ob.type == 'MESH': 748 | for v in ob.data.vertex_colors: 749 | if v.name == x: 750 | name = '{0}'.format(ob.name) 751 | 752 | if name not in AMTH_store_data.users['MESH_DATA']: 753 | AMTH_store_data.users['MESH_DATA'].append(name) 754 | # Check VCOL in Materials 755 | for ma in d.materials: 756 | # Cycles 757 | if utils.cycles_exists(): 758 | if ma and ma.node_tree and ma.node_tree.nodes: 759 | for no in ma.node_tree.nodes: 760 | if no and no.type in {'ATTRIBUTE'}: 761 | if no.attribute_name == x: 762 | objects = [] 763 | 764 | for ob in d.objects: 765 | if ma.name in ob.material_slots: 766 | objects.append(ob.name) 767 | 768 | if objects: 769 | name = '{0} in object: {1}'.format(ma.name, objects) 770 | else: 771 | name = '{0} (unassigned)'.format(ma.name) 772 | 773 | if name not in AMTH_store_data.users['MATERIAL']: 774 | AMTH_store_data.users['MATERIAL'].append(name) 775 | 776 | AMTH_store_data.libraries = sorted(list(set(AMTH_store_data.libraries))) 777 | 778 | # Print on console 779 | empty = True 780 | for t in AMTH_store_data.users: 781 | if AMTH_store_data.users[t]: 782 | empty = False 783 | print('\n== {0} {1} use {2} "{3}" ==\n'.format( 784 | len(AMTH_store_data.users[t]), 785 | t, 786 | dtype, 787 | x)) 788 | for p in AMTH_store_data.users[t]: 789 | print(' {0}'.format(p)) 790 | 791 | if AMTH_store_data.libraries: 792 | print_grammar("Check", "this library", "these libraries", 793 | AMTH_store_data.libraries 794 | ) 795 | print_with_count_list(send_list=AMTH_store_data.libraries) 796 | 797 | if empty: 798 | self.report({'INFO'}, "No users for {}".format(x)) 799 | 800 | return {"FINISHED"} 801 | 802 | 803 | class AMTH_SCENE_OT_list_users_debug_clear(Operator): 804 | """Clear the list bellow""" 805 | bl_idname = "scene.amth_list_users_debug_clear" 806 | bl_label = "Clear Debug Panel lists" 807 | 808 | what: StringProperty( 809 | name="", 810 | default="NONE", 811 | options={'HIDDEN'} 812 | ) 813 | 814 | def execute(self, context): 815 | reset_global_storage(self.what) 816 | 817 | return {"FINISHED"} 818 | 819 | 820 | class AMTH_SCENE_OT_blender_instance_open(Operator): 821 | """Open in a new Blender instance""" 822 | bl_idname = "scene.blender_instance_open" 823 | bl_label = "Open Blender Instance" 824 | 825 | filepath: StringProperty() 826 | 827 | def execute(self, context): 828 | if self.filepath: 829 | filepath = os.path.normpath(bpy.path.abspath(self.filepath)) 830 | 831 | import subprocess 832 | try: 833 | subprocess.Popen([bpy.app.binary_path, filepath]) 834 | except: 835 | print("Error opening a new Blender instance") 836 | import traceback 837 | traceback.print_exc() 838 | 839 | return {"FINISHED"} 840 | 841 | 842 | class AMTH_SCENE_OT_Collection_List_Refresh(Operator): 843 | bl_idname = "scene.amaranth_lighters_corner_refresh" 844 | bl_label = "Refresh" 845 | bl_description = ("Generate/Refresh the Lists\n" 846 | "Use to generate/refresh the list or after changes to Data") 847 | bl_options = {"REGISTER", "INTERNAL"} 848 | 849 | what: StringProperty(default="NONE") 850 | 851 | def execute(self, context): 852 | message = "No changes applied" 853 | 854 | if self.what == "LIGHTS": 855 | fill_ligters_corner_props(context, refresh=True) 856 | 857 | found_lights = len(context.window_manager.amth_lighters_state.keys()) 858 | message = "No Lights in the Data" if found_lights == 0 else \ 859 | "Generated list for {} found light(s)".format(found_lights) 860 | 861 | elif self.what == "IMAGES": 862 | fill_missing_images_props(context, refresh=True) 863 | 864 | found_images = len(context.window_manager.amth_missing_images_state.keys()) 865 | message = "Great! No missing Images" if found_images == 0 else \ 866 | "Missing {} image(s) in the Data".format(found_images) 867 | 868 | self.report({'INFO'}, message) 869 | 870 | return {"FINISHED"} 871 | 872 | 873 | class AMTH_SCENE_PT_scene_debug(Panel): 874 | """Scene Debug""" 875 | bl_label = "Scene Debug" 876 | bl_space_type = "PROPERTIES" 877 | bl_region_type = "WINDOW" 878 | bl_context = "scene" 879 | bl_options = {"DEFAULT_CLOSED"} 880 | 881 | def draw_header(self, context): 882 | layout = self.layout 883 | layout.label(text="", icon="RADIOBUT_ON") 884 | 885 | def draw_label(self, layout, body_text, single, multi, lists, ico="BLANK1"): 886 | layout.label( 887 | text="{} {} {}".format( 888 | str(len(lists)), body_text, 889 | single if len(lists) == 1 else multi), 890 | icon=ico 891 | ) 892 | 893 | def draw_miss_link(self, layout, text1, single, multi, text2, count, ico="BLANK1"): 894 | layout.label( 895 | text="{} {} {} {}".format( 896 | count, text1, 897 | single if count == 1 else multi, text2), 898 | icon=ico 899 | ) 900 | 901 | def draw(self, context): 902 | layout = self.layout 903 | scene = context.scene 904 | 905 | has_images = len(bpy.data.images) 906 | engine = scene.render.engine 907 | 908 | # List Missing Images 909 | box = layout.box() 910 | split = box.split(factor=0.8, align=True) 911 | row = split.row() 912 | 913 | if has_images: 914 | subrow = split.row(align=True) 915 | subrow.alignment = "RIGHT" 916 | subrow.operator(AMTH_SCENE_OT_Collection_List_Refresh.bl_idname, 917 | text="", icon="FILE_REFRESH").what = "IMAGES" 918 | image_state = context.window_manager.amth_missing_images_state 919 | 920 | row.label( 921 | text="{} Image Blocks present in the Data".format(has_images), 922 | icon="IMAGE_DATA" 923 | ) 924 | if len(image_state.keys()) > 0: 925 | box.template_list( 926 | 'AMTH_UL_MissingImages_UI', 927 | 'amth_collection_index_prop', 928 | context.window_manager, 929 | 'amth_missing_images_state', 930 | context.window_manager.amth_collection_index_prop, 931 | 'index_image', 932 | rows=3 933 | ) 934 | else: 935 | row.label(text="No images loaded yet", icon="RIGHTARROW_THIN") 936 | 937 | # List Cycles Materials by Shader 938 | if utils.cycles_exists() and engine == "CYCLES": 939 | box = layout.box() 940 | split = box.split() 941 | col = split.column(align=True) 942 | col.prop(scene, "amaranth_cycles_node_types", 943 | icon="MATERIAL") 944 | 945 | row = split.row(align=True) 946 | row.operator(AMTH_SCENE_OT_cycles_shader_list_nodes.bl_idname, 947 | icon="SORTSIZE", 948 | text="List Materials Using Shader") 949 | if len(AMTH_store_data.mat_shaders) != 0: 950 | row.operator( 951 | AMTH_SCENE_OT_list_users_debug_clear.bl_idname, 952 | icon="X", text="").what = "SHADER" 953 | col.separator() 954 | 955 | if len(AMTH_store_data.mat_shaders) != 0: 956 | col = box.column(align=True) 957 | self.draw_label(col, "found", "material", "materials", 958 | AMTH_store_data.mat_shaders, "INFO" 959 | ) 960 | for i, mat in enumerate(AMTH_store_data.mat_shaders): 961 | col.label( 962 | text="{}".format(AMTH_store_data.mat_shaders[i]), icon="MATERIAL" 963 | ) 964 | 965 | # List Missing Node Trees 966 | box = layout.box() 967 | row = box.row(align=True) 968 | split = row.split() 969 | col = split.column(align=True) 970 | 971 | split = col.split(align=True) 972 | split.label(text="Node Links") 973 | row = split.row(align=True) 974 | row.operator(AMTH_SCENE_OT_list_missing_node_links.bl_idname, 975 | icon="NODETREE") 976 | 977 | if AMTH_store_data.count_groups != 0 or \ 978 | AMTH_store_data.count_images != 0 or \ 979 | AMTH_store_data.count_image_node_unlinked != 0: 980 | 981 | row.operator( 982 | AMTH_SCENE_OT_list_users_debug_clear.bl_idname, 983 | icon="X", text="").what = "NODE_LINK" 984 | col.label(text="Warning! Check Console", icon="ERROR") 985 | 986 | if AMTH_store_data.count_groups != 0: 987 | self.draw_miss_link(col, "node", "group", "groups", "missing link", 988 | AMTH_store_data.count_groups, "NODE_TREE" 989 | ) 990 | if AMTH_store_data.count_images != 0: 991 | self.draw_miss_link(col, "image", "node", "nodes", "missing link", 992 | AMTH_store_data.count_images, "IMAGE_DATA" 993 | ) 994 | if AMTH_store_data.count_image_node_unlinked != 0: 995 | self.draw_miss_link(col, "image", "node", "nodes", "with no output conected", 996 | AMTH_store_data.count_image_node_unlinked, "NODE" 997 | ) 998 | 999 | # List Empty Materials Slots 1000 | box = layout.box() 1001 | split = box.split() 1002 | col = split.column(align=True) 1003 | col.label(text="Material Slots") 1004 | 1005 | row = split.row(align=True) 1006 | row.operator(AMTH_SCENE_OT_list_missing_material_slots.bl_idname, 1007 | icon="MATERIAL", 1008 | text="List Empty Materials Slots" 1009 | ) 1010 | if len(AMTH_store_data.obj_mat_slots) != 0: 1011 | row.operator( 1012 | AMTH_SCENE_OT_list_users_debug_clear.bl_idname, 1013 | icon="X", text="").what = "MAT_SLOTS" 1014 | 1015 | col.separator() 1016 | col = box.column(align=True) 1017 | self.draw_label(col, "found empty material slot", "object", "objects", 1018 | AMTH_store_data.obj_mat_slots, "INFO" 1019 | ) 1020 | for entry, obs in enumerate(AMTH_store_data.obj_mat_slots): 1021 | row = col.row() 1022 | row.alignment = "LEFT" 1023 | row.label( 1024 | text="{}".format(AMTH_store_data.obj_mat_slots[entry]), 1025 | icon="OBJECT_DATA") 1026 | 1027 | if AMTH_store_data.obj_mat_slots_lib: 1028 | col.separator() 1029 | col.label("Check {}:".format( 1030 | "this library" if 1031 | len(AMTH_store_data.obj_mat_slots_lib) == 1 else 1032 | "these libraries") 1033 | ) 1034 | for ilib, libs in enumerate(AMTH_store_data.obj_mat_slots_lib): 1035 | row = col.row(align=True) 1036 | row.alignment = "LEFT" 1037 | row.operator( 1038 | AMTH_SCENE_OT_blender_instance_open.bl_idname, 1039 | text=AMTH_store_data.obj_mat_slots_lib[ilib], 1040 | icon="LINK_BLEND", 1041 | emboss=False).filepath = AMTH_store_data.obj_mat_slots_lib[ilib] 1042 | 1043 | box = layout.box() 1044 | row = box.row(align=True) 1045 | row.label(text="List Users for Datablock") 1046 | 1047 | col = box.column(align=True) 1048 | split = col.split() 1049 | row = split.row(align=True) 1050 | row.prop( 1051 | scene, "amth_datablock_types", 1052 | icon=scene.amth_datablock_types, 1053 | text="" 1054 | ) 1055 | row.operator_menu_enum( 1056 | "scene.amth_list_users_for_x_type", 1057 | "list_type_select", 1058 | text=scene.amth_list_users_for_x_name 1059 | ) 1060 | 1061 | row = split.row(align=True) 1062 | row.enabled = True if USER_X_NAME_EMPTY not in scene.amth_list_users_for_x_name else False 1063 | row.operator( 1064 | AMTH_SCENE_OT_list_users_for_x.bl_idname, 1065 | icon="COLLAPSEMENU").name = scene.amth_list_users_for_x_name 1066 | 1067 | if any(val for val in AMTH_store_data.users.values()): 1068 | col = box.column(align=True) 1069 | 1070 | for t in AMTH_store_data.users: 1071 | 1072 | for ma in AMTH_store_data.users[t]: 1073 | subrow = col.row(align=True) 1074 | subrow.alignment = "LEFT" 1075 | 1076 | if t == 'OBJECT_DATA': 1077 | text_lib = " [L] " if \ 1078 | ma in bpy.data.objects and bpy.data.objects[ma].library else "" 1079 | subrow.operator( 1080 | AMTH_SCENE_OT_amaranth_object_select.bl_idname, 1081 | text="{} {}{}".format(text_lib, ma, 1082 | "" if ma in context.scene.objects else " [Not in Scene]"), 1083 | icon=t, 1084 | emboss=False).object_name = ma 1085 | else: 1086 | subrow.label(text=ma, icon=t) 1087 | row.operator( 1088 | AMTH_SCENE_OT_list_users_debug_clear.bl_idname, 1089 | icon="X", text="").what = "XTYPE" 1090 | 1091 | if AMTH_store_data.libraries: 1092 | count_lib = 0 1093 | 1094 | col.separator() 1095 | col.label("Check {}:".format( 1096 | "this library" if 1097 | len(AMTH_store_data.libraries) == 1 else 1098 | "these libraries") 1099 | ) 1100 | for libs in AMTH_store_data.libraries: 1101 | count_lib += 1 1102 | row = col.row(align=True) 1103 | row.alignment = "LEFT" 1104 | row.operator( 1105 | AMTH_SCENE_OT_blender_instance_open.bl_idname, 1106 | text=AMTH_store_data.libraries[count_lib - 1], 1107 | icon="LINK_BLEND", 1108 | emboss=False).filepath = AMTH_store_data.libraries[count_lib - 1] 1109 | 1110 | 1111 | class AMTH_PT_LightersCorner(Panel): 1112 | """The Lighters Panel""" 1113 | bl_label = "Lighter's Corner" 1114 | bl_idname = "AMTH_SCENE_PT_lighters_corner" 1115 | bl_space_type = 'PROPERTIES' 1116 | bl_region_type = 'WINDOW' 1117 | bl_context = "scene" 1118 | bl_options = {"DEFAULT_CLOSED"} 1119 | 1120 | def draw_header(self, context): 1121 | layout = self.layout 1122 | layout.label(text="", icon="LIGHT_SUN") 1123 | 1124 | def draw(self, context): 1125 | layout = self.layout 1126 | state_props = len(context.window_manager.amth_lighters_state) 1127 | engine = context.scene.render.engine 1128 | box = layout.box() 1129 | row = box.row(align=True) 1130 | 1131 | if utils.cycles_exists(): 1132 | row.prop(context.scene, "amaranth_lighterscorner_list_meshlights") 1133 | 1134 | subrow = row.row(align=True) 1135 | subrow.alignment = "RIGHT" 1136 | subrow.operator(AMTH_SCENE_OT_Collection_List_Refresh.bl_idname, 1137 | text="", icon="FILE_REFRESH").what = "LIGHTS" 1138 | 1139 | if not state_props: 1140 | row = box.row() 1141 | message = "Please Refresh" if len(bpy.data.lights) > 0 else "No Lights in Data" 1142 | row.label(text=message, icon="INFO") 1143 | else: 1144 | row = box.row(align=True) 1145 | split = row.split(factor=0.5, align=True) 1146 | col = split.column(align=True) 1147 | 1148 | col.label(text="Name/Library link") 1149 | 1150 | if engine in ["CYCLES", "BLENDER_RENDER"]: 1151 | splits = 0.6 if engine == "BLENDER_RENDER" else 0.4 1152 | splita = split.split(factor=splits, align=True) 1153 | col = splita.column(align=True) 1154 | col.alignment = "LEFT" 1155 | col.label(text="Samples") 1156 | 1157 | if utils.cycles_exists() and engine == "CYCLES": 1158 | col = splita.column(align=True) 1159 | col.label(text="Size") 1160 | 1161 | cols = row.row(align=True) 1162 | cols.alignment = "RIGHT" 1163 | cols.label(text="{}Render Visibility/Selection".format( 1164 | "Rays /" if utils.cycles_exists() else "") 1165 | ) 1166 | box.template_list( 1167 | 'AMTH_UL_LightersCorner_UI', 1168 | 'amth_collection_index_prop', 1169 | context.window_manager, 1170 | 'amth_lighters_state', 1171 | context.window_manager.amth_collection_index_prop, 1172 | 'index', 1173 | rows=5 1174 | ) 1175 | 1176 | 1177 | class AMTH_UL_MissingImages_UI(UIList): 1178 | 1179 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname): 1180 | text_lib = item.text_lib 1181 | has_filepath = item.has_filepath 1182 | is_library = item.is_library 1183 | 1184 | split = layout.split(factor=0.4) 1185 | row = split.row(align=True) 1186 | row.alignment = "LEFT" 1187 | row.label(text=text_lib, icon="IMAGE_DATA") 1188 | image = bpy.data.images.get(item.name, None) 1189 | 1190 | subrow = split.row(align=True) 1191 | splitp = subrow.split(factor=0.8, align=True).row(align=True) 1192 | splitp.alignment = "LEFT" 1193 | row_lib = subrow.row(align=True) 1194 | row_lib.alignment = "RIGHT" 1195 | if not image: 1196 | splitp.label(text="Image is not available", icon="ERROR") 1197 | else: 1198 | splitp.label(text=has_filepath, icon="LIBRARY_DATA_DIRECT") 1199 | if is_library: 1200 | row_lib.operator( 1201 | AMTH_SCENE_OT_blender_instance_open.bl_idname, 1202 | text="", 1203 | emboss=False, icon="LINK_BLEND").filepath = is_library 1204 | 1205 | 1206 | class AMTH_UL_LightersCorner_UI(UIList): 1207 | 1208 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname): 1209 | icon_type = item.icon_type 1210 | engine = context.scene.render.engine 1211 | text_lib = item.text_lib 1212 | is_library = item.is_library 1213 | 1214 | split = layout.split(factor=0.35) 1215 | row = split.row(align=True) 1216 | row.alignment = "LEFT" 1217 | row.label(text=text_lib, icon=icon_type) 1218 | ob = bpy.data.objects.get(item.name, None) 1219 | if not ob: 1220 | row.label(text="Object is not available", icon="ERROR") 1221 | else: 1222 | if is_library: 1223 | row.operator( 1224 | AMTH_SCENE_OT_blender_instance_open.bl_idname, 1225 | text="", 1226 | emboss=False, icon="LINK_BLEND").filepath = is_library 1227 | 1228 | rows = split.row(align=True) 1229 | splits = 0.9 if engine == "BLENDER_RENDER" else 0.4 1230 | splitlamp = rows.split(factor=splits, align=True) 1231 | splitlampb = splitlamp.row(align=True) 1232 | splitlampc = splitlamp.row(align=True) 1233 | splitlampd = rows.row(align=True) 1234 | splitlampd.alignment = "RIGHT" 1235 | 1236 | if utils.cycles_exists() and engine == "CYCLES": 1237 | if "LIGHT" in icon_type: 1238 | clamp = ob.data.cycles 1239 | if context.scene.cycles.progressive == "BRANCHED_PATH": 1240 | splitlampb.prop(clamp, "samples", text="") 1241 | if context.scene.cycles.progressive == "PATH": 1242 | splitlampb.label(text="N/A") 1243 | lamp = ob.data 1244 | if lamp.type in ["POINT", "SUN", "SPOT"]: 1245 | splitlampc.label(text="{:.2f}".format(lamp.shadow_soft_size)) 1246 | elif lamp.type == "HEMI": 1247 | splitlampc.label(text="N/A") 1248 | elif lamp.type == "AREA" and lamp.shape == "RECTANGLE": 1249 | splitlampc.label( 1250 | text="{:.2f} x {:.2f}".format(lamp.size, lamp.size_y) 1251 | ) 1252 | else: 1253 | splitlampc.label(text="{:.2f}".format(lamp.size)) 1254 | else: 1255 | splitlampb.label(text="N/A") 1256 | if engine == "BLENDER_RENDER": 1257 | if "LIGHT" in icon_type: 1258 | lamp = ob.data 1259 | if lamp.type == "HEMI": 1260 | splitlampb.label(text="Not Available") 1261 | elif lamp.type == "AREA" and lamp.shadow_method == "RAY_SHADOW": 1262 | splitlampb.prop(lamp, "shadow_ray_samples_x", text="X") 1263 | if lamp.shape == "RECTANGLE": 1264 | splitlampb.prop(lamp, "shadow_ray_samples_y", text="Y") 1265 | elif lamp.shadow_method == "RAY_SHADOW": 1266 | splitlampb.prop(lamp, "shadow_ray_samples", text="Ray Samples") 1267 | elif lamp.shadow_method == "BUFFER_SHADOW": 1268 | splitlampb.prop(lamp, "shadow_buffer_samples", text="Buffer Samples") 1269 | else: 1270 | splitlampb.label(text="No Shadow") 1271 | else: 1272 | splitlampb.label(text="N/A") 1273 | if utils.cycles_exists(): 1274 | visibility = ob.cycles_visibility 1275 | splitlampd.prop(visibility, "camera", text="") 1276 | splitlampd.prop(visibility, "diffuse", text="") 1277 | splitlampd.prop(visibility, "glossy", text="") 1278 | splitlampd.prop(visibility, "shadow", text="") 1279 | splitlampd.separator() 1280 | splitlampd.prop(ob, "hide", text="", emboss=False) 1281 | splitlampd.prop(ob, "hide_render", text="", emboss=False) 1282 | splitlampd.operator( 1283 | AMTH_SCENE_OT_amaranth_object_select.bl_idname, 1284 | text="", 1285 | emboss=False, icon="RESTRICT_SELECT_OFF").object_name = item.name 1286 | 1287 | 1288 | def fill_missing_images_props(context, refresh=False): 1289 | image_state = context.window_manager.amth_missing_images_state 1290 | if refresh: 1291 | for key in image_state.keys(): 1292 | index = image_state.find(key) 1293 | if index != -1: 1294 | image_state.remove(index) 1295 | 1296 | for im in bpy.data.images: 1297 | if im.type not in ("UV_TEST", "RENDER_RESULT", "COMPOSITING"): 1298 | if not im.packed_file and \ 1299 | not os.path.exists(bpy.path.abspath(im.filepath, library=im.library)): 1300 | text_l = "{}{} [{}]{}".format("[L] " if im.library else "", im.name, 1301 | im.users, " [F]" if im.use_fake_user else "") 1302 | prop = image_state.add() 1303 | prop.name = im.name 1304 | prop.text_lib = text_l 1305 | prop.has_filepath = im.filepath if im.filepath else "No Filepath" 1306 | prop.is_library = im.library.filepath if im.library else "" 1307 | 1308 | 1309 | def fill_ligters_corner_props(context, refresh=False): 1310 | light_state = context.window_manager.amth_lighters_state 1311 | list_meshlights = context.scene.amaranth_lighterscorner_list_meshlights 1312 | if refresh: 1313 | for key in light_state.keys(): 1314 | index = light_state.find(key) 1315 | if index != -1: 1316 | light_state.remove(index) 1317 | 1318 | for ob in bpy.data.objects: 1319 | if ob.name not in light_state.keys() or refresh: 1320 | is_light = ob.type == "LIGHT" 1321 | is_emission = True if utils.cycles_is_emission( 1322 | context, ob) and list_meshlights else False 1323 | 1324 | if is_light or is_emission: 1325 | icons = "LIGHT_%s" % ob.data.type if is_light else "MESH_GRID" 1326 | text_l = "{} {}{}".format(" [L] " if ob.library else "", ob.name, 1327 | "" if ob.name in context.scene.objects else " [Not in Scene]") 1328 | prop = light_state.add() 1329 | prop.name = ob.name 1330 | prop.icon_type = icons 1331 | prop.text_lib = text_l 1332 | prop.is_library = ob.library.filepath if ob.library else "" 1333 | 1334 | 1335 | class AMTH_LightersCornerStateProp(PropertyGroup): 1336 | icon_type: StringProperty() 1337 | text_lib: StringProperty() 1338 | is_library: StringProperty() 1339 | 1340 | 1341 | class AMTH_MissingImagesStateProp(PropertyGroup): 1342 | text_lib: StringProperty() 1343 | has_filepath: StringProperty() 1344 | is_library: StringProperty() 1345 | 1346 | 1347 | class AMTH_LightersCollectionIndexProp(PropertyGroup): 1348 | index: IntProperty( 1349 | name="index" 1350 | ) 1351 | index_image: IntProperty( 1352 | name="index" 1353 | ) 1354 | 1355 | 1356 | classes = ( 1357 | AMTH_SCENE_PT_scene_debug, 1358 | AMTH_SCENE_OT_list_users_debug_clear, 1359 | AMTH_SCENE_OT_blender_instance_open, 1360 | AMTH_SCENE_OT_amaranth_object_select, 1361 | AMTH_SCENE_OT_list_missing_node_links, 1362 | AMTH_SCENE_OT_list_missing_material_slots, 1363 | AMTH_SCENE_OT_cycles_shader_list_nodes, 1364 | AMTH_SCENE_OT_list_users_for_x, 1365 | AMTH_SCENE_OT_list_users_for_x_type, 1366 | AMTH_SCENE_OT_Collection_List_Refresh, 1367 | AMTH_LightersCornerStateProp, 1368 | AMTH_LightersCollectionIndexProp, 1369 | AMTH_MissingImagesStateProp, 1370 | AMTH_PT_LightersCorner, 1371 | AMTH_UL_LightersCorner_UI, 1372 | AMTH_UL_MissingImages_UI, 1373 | ) 1374 | 1375 | 1376 | def register(): 1377 | init() 1378 | 1379 | for cls in classes: 1380 | bpy.utils.register_class(cls) 1381 | 1382 | bpy.types.Scene.amth_list_users_for_x_name = StringProperty( 1383 | default="Select DataBlock Name", 1384 | name="Name", 1385 | description=USER_X_NAME_EMPTY, 1386 | options={"SKIP_SAVE"} 1387 | ) 1388 | bpy.types.WindowManager.amth_collection_index_prop = PointerProperty( 1389 | type=AMTH_LightersCollectionIndexProp 1390 | ) 1391 | bpy.types.WindowManager.amth_lighters_state = CollectionProperty( 1392 | type=AMTH_LightersCornerStateProp 1393 | ) 1394 | bpy.types.WindowManager.amth_missing_images_state = CollectionProperty( 1395 | type=AMTH_MissingImagesStateProp 1396 | ) 1397 | 1398 | 1399 | def unregister(): 1400 | clear() 1401 | 1402 | for cls in classes: 1403 | bpy.utils.unregister_class(cls) 1404 | 1405 | del bpy.types.Scene.amth_list_users_for_x_name 1406 | del bpy.types.WindowManager.amth_collection_index_prop 1407 | del bpy.types.WindowManager.amth_lighters_state 1408 | del bpy.types.WindowManager.amth_missing_images_state 1409 | -------------------------------------------------------------------------------- /scene/goto_library.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | File Browser: Libraries Bookmark 16 | 17 | The "Libraries" panel on the File Browser displays the path to all the 18 | libraries linked to that .blend. So you can quickly go to the folders 19 | related to the file. 20 | 21 | Click on any path to go to that directory. 22 | Developed during Caminandes Open Movie Project 23 | """ 24 | 25 | import bpy 26 | 27 | 28 | class AMTH_FILE_PT_libraries(bpy.types.Panel): 29 | bl_space_type = "FILE_BROWSER" 30 | bl_region_type = "TOOLS" 31 | bl_category = "Bookmarks" 32 | bl_label = "Libraries" 33 | 34 | def draw(self, context): 35 | layout = self.layout 36 | 37 | libs = bpy.data.libraries 38 | libslist = [] 39 | 40 | # Build the list of folders from libraries 41 | import os.path 42 | 43 | for lib in libs: 44 | directory_name = os.path.dirname(lib.filepath) 45 | libslist.append(directory_name) 46 | 47 | # Remove duplicates and sort by name 48 | libslist = set(libslist) 49 | libslist = sorted(libslist) 50 | 51 | # Draw the box with libs 52 | row = layout.row() 53 | box = row.box() 54 | 55 | if libslist: 56 | col = box.column() 57 | for filepath in libslist: 58 | if filepath != "//": 59 | row = col.row() 60 | row.alignment = "LEFT" 61 | props = row.operator( 62 | AMTH_FILE_OT_directory_go_to.bl_idname, 63 | text=filepath, icon="BOOKMARKS", 64 | emboss=False) 65 | props.filepath = filepath 66 | else: 67 | box.label(text="No libraries loaded") 68 | 69 | 70 | class AMTH_FILE_OT_directory_go_to(bpy.types.Operator): 71 | 72 | """Go to this library"s directory""" 73 | bl_idname = "file.directory_go_to" 74 | bl_label = "Go To" 75 | 76 | filepath: bpy.props.StringProperty(subtype="FILE_PATH") 77 | 78 | def execute(self, context): 79 | bpy.ops.file.select_bookmark(dir=self.filepath) 80 | return {"FINISHED"} 81 | 82 | 83 | def register(): 84 | bpy.utils.register_class(AMTH_FILE_PT_libraries) 85 | bpy.utils.register_class(AMTH_FILE_OT_directory_go_to) 86 | 87 | 88 | def unregister(): 89 | bpy.utils.unregister_class(AMTH_FILE_PT_libraries) 90 | bpy.utils.unregister_class(AMTH_FILE_OT_directory_go_to) 91 | -------------------------------------------------------------------------------- /scene/material_remove_unassigned.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | import bpy 15 | 16 | 17 | # FEATURE: Delete Materials not assigned to any verts 18 | class AMTH_OBJECT_OT_material_remove_unassigned(bpy.types.Operator): 19 | 20 | """Remove materials not assigned to any vertex""" 21 | bl_idname = "object.amaranth_object_material_remove_unassigned" 22 | bl_label = "Remove Unassigned Materials" 23 | 24 | @classmethod 25 | def poll(cls, context): 26 | return context.active_object.material_slots 27 | 28 | def execute(self, context): 29 | 30 | scene = context.scene 31 | act_ob = context.active_object 32 | count = len(act_ob.material_slots) 33 | materials_removed = [] 34 | act_ob.active_material_index = 0 35 | is_visible = True 36 | 37 | if act_ob not in context.visible_objects: 38 | is_visible = False 39 | n = -1 40 | for lay in act_ob.layers: 41 | n += 1 42 | if lay: 43 | break 44 | 45 | scene.layers[n] = True 46 | 47 | for slot in act_ob.material_slots: 48 | count -= 1 49 | 50 | bpy.ops.object.mode_set(mode="EDIT") 51 | bpy.ops.mesh.select_all(action="DESELECT") 52 | act_ob.active_material_index = count 53 | bpy.ops.object.material_slot_select() 54 | 55 | if act_ob.data.total_vert_sel == 0 or \ 56 | (len(act_ob.material_slots) == 1 and not 57 | act_ob.material_slots[0].material): 58 | materials_removed.append( 59 | "%s" % 60 | act_ob.active_material.name if act_ob.active_material else "Empty") 61 | bpy.ops.object.mode_set(mode="OBJECT") 62 | bpy.ops.object.material_slot_remove() 63 | else: 64 | pass 65 | 66 | bpy.ops.object.mode_set(mode="EDIT") 67 | bpy.ops.mesh.select_all(action="DESELECT") 68 | bpy.ops.object.mode_set(mode="OBJECT") 69 | 70 | if materials_removed: 71 | print( 72 | "\n* Removed %s Unassigned Materials \n" % 73 | len(materials_removed)) 74 | 75 | count_mr = 0 76 | 77 | for mr in materials_removed: 78 | count_mr += 1 79 | print( 80 | "%0.2d. %s" % 81 | (count_mr, materials_removed[count_mr - 1])) 82 | 83 | print("\n") 84 | self.report({"INFO"}, "Removed %s Unassigned Materials" % 85 | len(materials_removed)) 86 | 87 | if not is_visible: 88 | scene.layers[n] = False 89 | 90 | return {"FINISHED"} 91 | 92 | 93 | def ui_material_remove_unassigned(self, context): 94 | self.layout.operator( 95 | AMTH_OBJECT_OT_material_remove_unassigned.bl_idname, 96 | icon="X") 97 | 98 | # // FEATURE: Delete Materials not assigned to any verts 99 | 100 | 101 | def register(): 102 | bpy.utils.register_class(AMTH_OBJECT_OT_material_remove_unassigned) 103 | bpy.types.MATERIAL_MT_context_menu.append(ui_material_remove_unassigned) 104 | 105 | 106 | def unregister(): 107 | bpy.utils.unregister_class(AMTH_OBJECT_OT_material_remove_unassigned) 108 | bpy.types.MATERIAL_MT_context_menu.remove(ui_material_remove_unassigned) 109 | -------------------------------------------------------------------------------- /scene/refresh.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Refresh Scene 16 | 17 | Refresh the current scene, useful when working with libraries or drivers. 18 | Could also add an option to refresh the VSE maybe? Usage: Hit F5 or find 19 | it on the Specials menu W. 20 | """ 21 | 22 | import bpy 23 | 24 | 25 | KEYMAPS = list() 26 | 27 | 28 | class AMTH_SCENE_OT_refresh(bpy.types.Operator): 29 | """Refresh the current scene""" 30 | bl_idname = "scene.refresh" 31 | bl_label = "Refresh!" 32 | 33 | def execute(self, context): 34 | get_addon = "amaranth" in context.preferences.addons.keys() 35 | if not get_addon: 36 | return {"CANCELLED"} 37 | 38 | preferences = context.preferences.addons["amaranth"].preferences 39 | scene = context.scene 40 | 41 | if preferences.use_scene_refresh: 42 | # Changing the frame is usually the best way to go 43 | scene.frame_current = scene.frame_current 44 | self.report({"INFO"}, "Scene Refreshed!") 45 | 46 | return {"FINISHED"} 47 | 48 | 49 | def button_refresh(self, context): 50 | get_addon = "amaranth" in context.preferences.addons.keys() 51 | if not get_addon: 52 | return 53 | 54 | if context.preferences.addons["amaranth"].preferences.use_scene_refresh: 55 | self.layout.separator() 56 | self.layout.operator(AMTH_SCENE_OT_refresh.bl_idname, 57 | text="Refresh!", 58 | icon="FILE_REFRESH") 59 | 60 | 61 | def register(): 62 | bpy.utils.register_class(AMTH_SCENE_OT_refresh) 63 | bpy.types.VIEW3D_MT_object_context_menu.append(button_refresh) 64 | kc = bpy.context.window_manager.keyconfigs.addon 65 | km = kc.keymaps.new(name="Window") 66 | kmi = km.keymap_items.new("scene.refresh", "F5", "PRESS", 67 | alt=True) 68 | KEYMAPS.append((km, kmi)) 69 | 70 | 71 | def unregister(): 72 | bpy.utils.unregister_class(AMTH_SCENE_OT_refresh) 73 | bpy.types.VIEW3D_MT_object_context_menu.remove(button_refresh) 74 | for km, kmi in KEYMAPS: 75 | km.keymap_items.remove(kmi) 76 | KEYMAPS.clear() 77 | -------------------------------------------------------------------------------- /scene/save_reload.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Save & Reload File 16 | 17 | When working with linked libraries, very often you need to save and load 18 | again to see the changes. 19 | This does it in one go, without asking, so be careful :) 20 | Usage: Hit Ctrl + Shift + W or find it at the bottom of the File menu. 21 | """ 22 | 23 | import bpy 24 | 25 | 26 | KEYMAPS = list() 27 | 28 | 29 | class AMTH_WM_OT_save_reload(bpy.types.Operator): 30 | """Save and Reload the current blend file""" 31 | bl_idname = "wm.save_reload" 32 | bl_label = "Save & Reload" 33 | 34 | def save_reload(self, context, path): 35 | if not path: 36 | bpy.ops.wm.save_as_mainfile("INVOKE_AREA") 37 | return 38 | bpy.ops.wm.save_mainfile() 39 | self.report({"INFO"}, "Saved & Reloaded") 40 | bpy.ops.wm.open_mainfile("EXEC_DEFAULT", filepath=path) 41 | 42 | def execute(self, context): 43 | path = bpy.data.filepath 44 | self.save_reload(context, path) 45 | 46 | return {"FINISHED"} 47 | 48 | 49 | def button_save_reload(self, context): 50 | get_addon = "amaranth" in context.preferences.addons.keys() 51 | if not get_addon: 52 | return 53 | 54 | if context.preferences.addons["amaranth"].preferences.use_file_save_reload: 55 | self.layout.separator() 56 | self.layout.operator( 57 | AMTH_WM_OT_save_reload.bl_idname, 58 | text="Save & Reload", 59 | icon="FILE_REFRESH") 60 | 61 | 62 | def register(): 63 | bpy.utils.register_class(AMTH_WM_OT_save_reload) 64 | bpy.types.TOPBAR_MT_file.append(button_save_reload) 65 | wm = bpy.context.window_manager 66 | kc = wm.keyconfigs.addon 67 | km = kc.keymaps.new(name="Window") 68 | kmi = km.keymap_items.new("wm.save_reload", "W", "PRESS", 69 | shift=True, ctrl=True) 70 | KEYMAPS.append((km, kmi)) 71 | 72 | 73 | def unregister(): 74 | bpy.utils.unregister_class(AMTH_WM_OT_save_reload) 75 | bpy.types.TOPBAR_MT_file.remove(button_save_reload) 76 | for km, kmi in KEYMAPS: 77 | km.keymap_items.remove(kmi) 78 | KEYMAPS.clear() 79 | -------------------------------------------------------------------------------- /scene/stats.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | """ 15 | Scene, Cameras, and Meshlights Count 16 | 17 | Increase the stats by displaying the number of scenes, cameras, and light 18 | emitting meshes. 19 | On the Info header. 20 | """ 21 | 22 | import bpy 23 | from amaranth import utils 24 | 25 | 26 | def stats_scene(self, context): 27 | get_addon = "amaranth" in context.preferences.addons.keys() 28 | if not get_addon: 29 | return 30 | 31 | if context.preferences.addons["amaranth"].preferences.use_scene_stats: 32 | scenes_count = str(len(bpy.data.scenes)) 33 | cameras_count = str(len(bpy.data.cameras)) 34 | cameras_selected = 0 35 | meshlights = 0 36 | meshlights_visible = 0 37 | 38 | for ob in context.scene.objects: 39 | if utils.cycles_is_emission(context, ob): 40 | meshlights += 1 41 | if ob in context.visible_objects: 42 | meshlights_visible += 1 43 | 44 | if ob in context.selected_objects: 45 | if ob.type == 'CAMERA': 46 | cameras_selected += 1 47 | 48 | meshlights_string = '| Meshlights:{}/{}'.format( 49 | meshlights_visible, meshlights) 50 | 51 | row = self.layout.row(align=True) 52 | row.label(text="Scenes:{} | Cameras:{}/{} {}".format( 53 | scenes_count, cameras_selected, cameras_count, 54 | meshlights_string if utils.cycles_active(context) else '')) 55 | 56 | 57 | def register(): 58 | bpy.types.STATUSBAR_HT_header.append(stats_scene) 59 | 60 | 61 | def unregister(): 62 | bpy.types.STATUSBAR_HT_header.remove(stats_scene) 63 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or 2 | # modify it under the terms of the GNU General Public License 3 | # as published by the Free Software Foundation; either version 2 4 | # of the License, or (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software Foundation, 13 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | 15 | import bpy 16 | 17 | 18 | # FUNCTION: Checks if cycles is available 19 | def cycles_exists(): 20 | return hasattr(bpy.types.Scene, "cycles") 21 | 22 | 23 | # FUNCTION: Checks if cycles is the active renderer 24 | def cycles_active(context): 25 | return context.scene.render.engine == "CYCLES" 26 | 27 | 28 | # FUNCTION: Check if material has Emission (for select and stats) 29 | def cycles_is_emission(context, ob): 30 | is_emission = False 31 | 32 | if not ob.material_slots: 33 | return is_emission 34 | 35 | for ma in ob.material_slots: 36 | if not ma.material: 37 | continue 38 | if ma.material.node_tree and ma.material.node_tree.nodes: 39 | for no in ma.material.node_tree.nodes: 40 | if not no.type in ("EMISSION", "GROUP"): 41 | continue 42 | for ou in no.outputs: 43 | if not ou.links: 44 | continue 45 | if no.type == "GROUP" and no.node_tree and no.node_tree.nodes: 46 | for gno in no.node_tree.nodes: 47 | if gno.type != "EMISSION": 48 | continue 49 | for gou in gno.outputs: 50 | if ou.links and gou.links: 51 | is_emission = True 52 | elif no.type == "EMISSION": 53 | if ou.links: 54 | is_emission = True 55 | return is_emission 56 | 57 | 58 | # FUNCTION: Check if object has keyframes for a specific frame 59 | def is_keyframe(ob, frame): 60 | if ob is not None and ob.animation_data is not None and ob.animation_data.action is not None: 61 | for fcu in ob.animation_data.action.fcurves: 62 | if frame in (p.co.x for p in fcu.keyframe_points): 63 | return True 64 | return False 65 | --------------------------------------------------------------------------------