diff options
author | CansecoGPC <machaquiro@yahoo.es> | 2019-12-09 16:42:04 +0300 |
---|---|---|
committer | CansecoGPC <machaquiro@yahoo.es> | 2019-12-09 16:42:04 +0300 |
commit | 75af6e5dcf84cc2d2693374a01ecbad0f874701b (patch) | |
tree | 86d5ad098857a591e9f997881d762e839b33e98a /amaranth | |
parent | 395ca8a4be7a66c72a5556c51f958644601a846b (diff) |
Amaranth: Add back from addons contrib
Diffstat (limited to 'amaranth')
42 files changed, 4973 insertions, 0 deletions
diff --git a/amaranth/__init__.py b/amaranth/__init__.py new file mode 100644 index 00000000..3a4dd49f --- /dev/null +++ b/amaranth/__init__.py @@ -0,0 +1,119 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Amaranth + +Using Blender every day, you get to change little things on it to speedup +your workflow. The problem is when you have to switch computers with +somebody else's Blender, it sucks. +That's the main reason behind Amaranth. I ported all sort of little changes +I find useful into this addon. + +What is it about? Anything, whatever I think it can speedup workflow, +I'll try to add it. Enjoy <3 +""" + +import sys + +# import amaranth's modules + +# NOTE: avoid local imports whenever possible! +# Thanks to Christopher Crouzet for let me know about this. +# http://stackoverflow.com/questions/13392038/python-making-a-class-variable-static-even-when-a-module-is-imported-in-differe + +from amaranth import prefs + +from amaranth.modeling import symmetry_tools + +from amaranth.scene import ( + refresh, + save_reload, + current_blend, + stats, + goto_library, + debug, + material_remove_unassigned, + ) + +from amaranth.node_editor import ( + id_panel, + display_image, + templates, + simplify_nodes, + node_stats, + normal_node, + switch_material, + node_shader_extra, + ) + +from amaranth.render import ( + border_camera, + meshlight_add, + meshlight_select, + passepartout, + final_resolution, + samples_scene, + render_output_z, + ) + +from amaranth.animation import ( + timeline_extra_info, + frame_current, + motion_paths, + jump_frames, + ) + +from amaranth.misc import ( + color_management, + dupli_group_id, + toggle_wire, + sequencer_extra_info, + ) + + +# register the addon + modules found in globals() +bl_info = { + "name": "Amaranth Toolset", + "author": "Pablo Vazquez, Bassam Kurdali, Sergey Sharybin, Lukas Tönne, Cesar Saez, CansecoGPC", + "version": (1, 0, 8), + "blender": (2, 81, 0), + "location": "Everywhere!", + "description": "A collection of tools and settings to improve productivity", + "warning": "", + "wiki_url": "https://pablovazquez.art/amaranth", + "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/", + "category": "Interface", +} + + +def _call_globals(attr_name): + for m in globals().values(): + if hasattr(m, attr_name): + getattr(m, attr_name)() + + +def _flush_modules(pkg_name): + pkg_name = pkg_name.lower() + for k in tuple(sys.modules.keys()): + if k.lower().startswith(pkg_name): + del sys.modules[k] + + +def register(): + _call_globals("register") + + +def unregister(): + _call_globals("unregister") + _flush_modules("amaranth") # reload amaranth diff --git a/amaranth/animation/__init__.py b/amaranth/animation/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/amaranth/animation/__init__.py diff --git a/amaranth/animation/frame_current.py b/amaranth/animation/frame_current.py new file mode 100644 index 00000000..6bea2009 --- /dev/null +++ b/amaranth/animation/frame_current.py @@ -0,0 +1,45 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Current Frame Slider + +Currently the only way to change the current frame is to have a Timeline +editor open, but sometimes you don't have one, or you're fullscreen. +This option adds the Current Frame slider to the Specials menu. Find it +hitting the W menu in Object mode, you can slide or click in the middle +of the button to set the frame manually. +""" + +import bpy + + +def button_frame_current(self, context): + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return + + scene = context.scene + if context.preferences.addons["amaranth"].preferences.use_frame_current: + self.layout.separator() + self.layout.prop(scene, "frame_current", text="Set Current Frame") + + +def register(): + bpy.types.VIEW3D_MT_object_context_menu.append(button_frame_current) + bpy.types.VIEW3D_MT_pose_context_menu.append(button_frame_current) + + +def unregister(): + bpy.types.VIEW3D_MT_object_context_menu.remove(button_frame_current) + bpy.types.VIEW3D_MT_pose_context_menu.remove(button_frame_current) diff --git a/amaranth/animation/jump_frames.py b/amaranth/animation/jump_frames.py new file mode 100644 index 00000000..fb12cb35 --- /dev/null +++ b/amaranth/animation/jump_frames.py @@ -0,0 +1,209 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Jump X Frames on Shift Up/Down + +When you hit Shift Up/Down, you'll jump 10 frames forward/backwards. +Sometimes is nice to tweak that value. + +In the User Preferences, Editing tab, you'll find a "Frames to Jump" +slider where you can adjust how many frames you'd like to move +forwards/backwards. + +Make sure you save your user settings if you want to use this value from +now on. + +Find it on the User Preferences, Editing. +""" + +import bpy +from bpy.types import Operator +from bpy.props import BoolProperty + +KEYMAPS = list() + + +# FUNCTION: Check if object has keyframes for a specific frame +def is_keyframe(ob, frame): + if ob is not None and ob.animation_data is not None and ob.animation_data.action is not None: + for fcu in ob.animation_data.action.fcurves: + if frame in (p.co.x for p in fcu.keyframe_points): + return True + return False + + +# monkey path is_keyframe function +bpy.types.Object.is_keyframe = is_keyframe + + +# FEATURE: Jump to frame in-between next and previous keyframe +class AMTH_SCREEN_OT_keyframe_jump_inbetween(Operator): + """Jump to half in-between keyframes""" + bl_idname = "screen.amth_keyframe_jump_inbetween" + bl_label = "Jump to Keyframe In-between" + + backwards: BoolProperty() + + def execute(self, context): + back = self.backwards + + scene = context.scene + ob = bpy.context.object + frame_start = scene.frame_start + frame_end = scene.frame_end + + if not context.scene.get("amth_keyframes_jump"): + context.scene["amth_keyframes_jump"] = list() + + keyframes_list = context.scene["amth_keyframes_jump"] + + for f in range(frame_start, frame_end): + if ob.is_keyframe(f): + keyframes_list = list(keyframes_list) + keyframes_list.append(f) + + if keyframes_list: + keyframes_list_half = [] + + for i, item in enumerate(keyframes_list): + try: + next_item = keyframes_list[i + 1] + keyframes_list_half.append(int((item + next_item) / 2)) + except: + pass + + if len(keyframes_list_half) > 1: + if back: + v = (scene.frame_current == keyframes_list_half[::-1][-1], + scene.frame_current < keyframes_list_half[::-1][-1]) + if any(v): + self.report({"INFO"}, "No keyframes behind") + else: + for i in keyframes_list_half[::-1]: + if scene.frame_current > i: + scene.frame_current = i + break + else: + v = (scene.frame_current == keyframes_list_half[-1], + scene.frame_current > keyframes_list_half[-1]) + if any(v): + self.report({"INFO"}, "No keyframes ahead") + else: + for i in keyframes_list_half: + if scene.frame_current < i: + scene.frame_current = i + break + else: + self.report({"INFO"}, "Object has only 1 keyframe") + else: + self.report({"INFO"}, "Object has no keyframes") + + return {"FINISHED"} + + +# FEATURE: Jump forward/backward every N frames +class AMTH_SCREEN_OT_frame_jump(Operator): + """Jump a number of frames forward/backwards""" + bl_idname = "screen.amaranth_frame_jump" + bl_label = "Jump Frames" + + forward: BoolProperty(default=True) + + def execute(self, context): + scene = context.scene + + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return {"CANCELLED"} + + preferences = context.preferences.addons["amaranth"].preferences + + if preferences.use_framerate: + framedelta = scene.render.fps + else: + framedelta = preferences.frames_jump + if self.forward: + scene.frame_current = scene.frame_current + framedelta + else: + scene.frame_current = scene.frame_current - framedelta + + return {"FINISHED"} + + +def ui_userpreferences_edit(self, context): + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return + + preferences = context.preferences.addons["amaranth"].preferences + + col = self.layout.column() + split = col.split(factor=0.21) + split.prop(preferences, "frames_jump", + text="Frames to Jump") + + +def label(self, context): + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return + + layout = self.layout + + if context.preferences.addons["amaranth"].preferences.use_timeline_extra_info: + row = layout.row(align=True) + + row.operator(AMTH_SCREEN_OT_keyframe_jump_inbetween.bl_idname, + icon="PREV_KEYFRAME", text="").backwards = True + row.operator(AMTH_SCREEN_OT_keyframe_jump_inbetween.bl_idname, + icon="NEXT_KEYFRAME", text="").backwards = False + + +def register(): + bpy.utils.register_class(AMTH_SCREEN_OT_frame_jump) + bpy.utils.register_class(AMTH_SCREEN_OT_keyframe_jump_inbetween) + bpy.types.USERPREF_PT_animation_timeline.append(ui_userpreferences_edit) + bpy.types.USERPREF_PT_animation_timeline.append(label) + + # register keyboard shortcuts + wm = bpy.context.window_manager + kc = wm.keyconfigs.addon + + km = kc.keymaps.new(name="Frames") + kmi = km.keymap_items.new('screen.amth_keyframe_jump_inbetween', 'UP_ARROW', 'PRESS', shift=True, ctrl=True) + kmi.properties.backwards = False + KEYMAPS.append((km, kmi)) + + kmi = km.keymap_items.new('screen.amth_keyframe_jump_inbetween', 'DOWN_ARROW', 'PRESS', shift=True, ctrl=True) + kmi.properties.backwards = True + KEYMAPS.append((km, kmi)) + + kmi = km.keymap_items.new( + "screen.amaranth_frame_jump", "UP_ARROW", "PRESS", shift=True) + kmi.properties.forward = True + KEYMAPS.append((km, kmi)) + + kmi = km.keymap_items.new( + "screen.amaranth_frame_jump", "DOWN_ARROW", "PRESS", shift=True) + kmi.properties.forward = False + KEYMAPS.append((km, kmi)) + + +def unregister(): + bpy.utils.unregister_class(AMTH_SCREEN_OT_frame_jump) + bpy.utils.unregister_class(AMTH_SCREEN_OT_keyframe_jump_inbetween) + bpy.types.USERPREF_PT_animation_timeline.remove(ui_userpreferences_edit) + for km, kmi in KEYMAPS: + km.keymap_items.remove(kmi) + KEYMAPS.clear() diff --git a/amaranth/animation/motion_paths.py b/amaranth/animation/motion_paths.py new file mode 100644 index 00000000..7988b452 --- /dev/null +++ b/amaranth/animation/motion_paths.py @@ -0,0 +1,144 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Bone Motion Paths: + +Match Frame Range + Clear All Paths + +* Clear All Paths: +Silly operator to loop through all bones and clear their paths, useful +when having hidden bones (othrewise you have to go through each one of +them and clear manually) + +*Match Current Frame Range: +Set the current frame range as motion path range. + +Both requests by Hjalti from Project Pampa +Thanks to Bassam Kurdali for helping finding out the weirdness behind +Motion Paths bpy. + +Developed during Caminandes Open Movie Project +""" + +import bpy + + +class AMTH_POSE_OT_paths_clear_all(bpy.types.Operator): + + """Clear motion paths from all bones""" + bl_idname = "pose.paths_clear_all" + bl_label = "Clear All Motion Paths" + bl_options = {"UNDO"} + + @classmethod + def poll(cls, context): + return context.mode == "POSE" + + def execute(self, context): + # silly but works + for b in context.object.data.bones: + b.select = True + bpy.ops.pose.paths_clear() + b.select = False + return {"FINISHED"} + + +class AMTH_POSE_OT_paths_frame_match(bpy.types.Operator): + + """Match Start/End frame of scene to motion path range""" + bl_idname = "pose.paths_frame_match" + bl_label = "Match Frame Range" + bl_options = {"UNDO"} + + def execute(self, context): + avs = context.object.pose.animation_visualization + scene = context.scene + + if avs.motion_path.type == "RANGE": + if scene.use_preview_range: + avs.motion_path.frame_start = scene.frame_preview_start + avs.motion_path.frame_end = scene.frame_preview_end + else: + avs.motion_path.frame_start = scene.frame_start + avs.motion_path.frame_end = scene.frame_end + + else: + if scene.use_preview_range: + avs.motion_path.frame_before = scene.frame_preview_start + avs.motion_path.frame_after = scene.frame_preview_end + else: + avs.motion_path.frame_before = scene.frame_start + avs.motion_path.frame_after = scene.frame_end + + return {"FINISHED"} + + +def pose_motion_paths_ui(self, context): + + layout = self.layout + scene = context.scene + avs = context.object.pose.animation_visualization + if context.active_pose_bone: + mpath = context.active_pose_bone.motion_path + layout.separator() + layout.label(text="Motion Paths Extras:") + + split = layout.split() + + col = split.column(align=True) + + if context.selected_pose_bones: + if mpath: + sub = col.row(align=True) + sub.operator( + "pose.paths_update", text="Update Path", icon="BONE_DATA") + sub.operator("pose.paths_clear", text="", icon="X") + else: + col.operator( + "pose.paths_calculate", + text="Calculate Path", + icon="BONE_DATA") + else: + col.label(text="Select Bones First", icon="ERROR") + + col = split.column(align=True) + col.operator( + AMTH_POSE_OT_paths_frame_match.bl_idname, + text="Set Preview Frame Range" if scene.use_preview_range else "Set Frame Range", + icon="PREVIEW_RANGE" if scene.use_preview_range else "TIME") + + col = layout.column() + row = col.row(align=True) + + if avs.motion_path.type == "RANGE": + row.prop(avs.motion_path, "frame_start", text="Start") + row.prop(avs.motion_path, "frame_end", text="End") + else: + row.prop(avs.motion_path, "frame_before", text="Before") + row.prop(avs.motion_path, "frame_after", text="After") + + layout.separator() + layout.operator(AMTH_POSE_OT_paths_clear_all.bl_idname, icon="X") + + +def register(): + bpy.utils.register_class(AMTH_POSE_OT_paths_clear_all) + bpy.utils.register_class(AMTH_POSE_OT_paths_frame_match) + bpy.types.DATA_PT_display.append(pose_motion_paths_ui) + + +def unregister(): + bpy.utils.unregister_class(AMTH_POSE_OT_paths_clear_all) + bpy.utils.unregister_class(AMTH_POSE_OT_paths_frame_match) + bpy.types.DATA_PT_display.remove(pose_motion_paths_ui) diff --git a/amaranth/animation/timeline_extra_info.py b/amaranth/animation/timeline_extra_info.py new file mode 100644 index 00000000..0e875b43 --- /dev/null +++ b/amaranth/animation/timeline_extra_info.py @@ -0,0 +1,67 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Timeline Extra Info + +Display amount of frames left until Frame End, very handy especially when +rendering an animation or OpenGL preview. +Display current/end time on SMPTE. Find it on the Timeline header. +""" + +import bpy + + +def label_timeline_extra_info(self, context): + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return + + layout = self.layout + scene = context.scene + + if context.preferences.addons["amaranth"].preferences.use_timeline_extra_info: + row = layout.row(align=True) + + # Check for preview range + frame_start = scene.frame_preview_start if scene.use_preview_range else scene.frame_start + frame_end = scene.frame_preview_end if scene.use_preview_range else scene.frame_end + + row.label( + text="%s / %s" % + (bpy.utils.smpte_from_frame( + scene.frame_current - + frame_start), + bpy.utils.smpte_from_frame( + frame_end - + frame_start))) + + if (scene.frame_current > frame_end): + row.label(text="%s Frames Ahead" % + ((frame_end - scene.frame_current) * -1)) + elif (scene.frame_current == frame_start): + row.label(text="Start Frame (%s left)" % + (frame_end - scene.frame_current)) + elif (scene.frame_current == frame_end): + row.label(text="%s End Frame" % scene.frame_current) + else: + row.label(text="%s Frames Left" % + (frame_end - scene.frame_current)) + + +def register(): + bpy.types.STATUSBAR_HT_header.append(label_timeline_extra_info) + + +def unregister(): + bpy.types.STATUSBAR_HT_header.remove(label_timeline_extra_info) diff --git a/amaranth/misc/__init__.py b/amaranth/misc/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/amaranth/misc/__init__.py diff --git a/amaranth/misc/color_management.py b/amaranth/misc/color_management.py new file mode 100644 index 00000000..72ad4bb2 --- /dev/null +++ b/amaranth/misc/color_management.py @@ -0,0 +1,84 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Color Management Presets + +Save your Color Management options as presets, for easy re-use. + +It will pretty much every option in the Color Management panel, such as +the look, color settings, and so on. Except the curve points (have to +figure out how to do that nicely), good news is that in Blender 2.69+ you +can now copy/paste curves. +""" + +import bpy +from bl_operators.presets import AddPresetBase + + +class AMTH_SCENE_MT_color_management_presets(bpy.types.Menu): + + """List of Color Management presets""" + bl_label = "Color Management Presets" + preset_subdir = "color" + preset_operator = "script.execute_preset" + draw = bpy.types.Menu.draw_preset + + +class AMTH_AddPresetColorManagement(AddPresetBase, bpy.types.Operator): + + """Add or remove a Color Management preset""" + bl_idname = "scene.color_management_preset_add" + bl_label = "Add Color Management Preset" + preset_menu = "AMTH_SCENE_MT_color_management_presets" + + preset_defines = [ + "scene = bpy.context.scene", + ] + + preset_values = [ + "scene.view_settings.view_transform", + "scene.display_settings.display_device", + "scene.view_settings.exposure", + "scene.view_settings.gamma", + "scene.view_settings.look", + "scene.view_settings.use_curve_mapping", + "scene.sequencer_colorspace_settings.name", + ] + + preset_subdir = "color" + + +def ui_color_management_presets(self, context): + + layout = self.layout + + row = layout.row(align=True) + row.menu("AMTH_SCENE_MT_color_management_presets", + text=bpy.types.AMTH_SCENE_MT_color_management_presets.bl_label) + row.operator("scene.color_management_preset_add", text="", icon="ZOOM_IN") + row.operator("scene.color_management_preset_add", + text="", icon="ZOOM_OUT").remove_active = True + layout.separator() + + +def register(): + bpy.utils.register_class(AMTH_AddPresetColorManagement) + bpy.utils.register_class(AMTH_SCENE_MT_color_management_presets) + bpy.types.RENDER_PT_color_management.prepend(ui_color_management_presets) + + +def unregister(): + bpy.utils.unregister_class(AMTH_AddPresetColorManagement) + bpy.utils.unregister_class(AMTH_SCENE_MT_color_management_presets) + bpy.types.RENDER_PT_color_management.remove(ui_color_management_presets) diff --git a/amaranth/misc/dupli_group_id.py b/amaranth/misc/dupli_group_id.py new file mode 100644 index 00000000..05fa7129 --- /dev/null +++ b/amaranth/misc/dupli_group_id.py @@ -0,0 +1,197 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Object ID for Dupli Groups +Say you have a linked character or asset, you can now set an Object ID for the +entire instance (the objects in the group), and use it with the Object Index +pass later in compositing. Something that I always wanted and it wasn't +possible! + +In order for the Object ID to be loaded afterwards on computers without +Amaranth installed, it will automatically create a text file (called +AmaranthStartup.py) and save it inside the .blend, this will autorun on +startup and set the OB IDs. Remember to have auto-run python scripts on your +startup preferences. + +Set a Pass Index and press "Apply Object ID to Duplis" on the Relations panel, +Object Properties. +""" + + +import bpy +from amaranth.scene.debug import AMTH_SCENE_OT_blender_instance_open + + +# Some settings are bound to be saved on a startup py file +# TODO: refactor this, amth_text should not be declared as a global variable, +# otherwise becomes confusing when you call it in the classes below. +def amaranth_text_startup(context): + + amth_text_name = "AmaranthStartup.py" + amth_text_exists = False + + global amth_text + + try: + if bpy.data.texts: + for tx in bpy.data.texts: + if tx.name == amth_text_name: + amth_text_exists = True + amth_text = bpy.data.texts[amth_text_name] + break + else: + amth_text_exists = False + + if not amth_text_exists: + bpy.ops.text.new() + amth_text = bpy.data.texts[((len(bpy.data.texts) * -1) + 1)] + amth_text.name = amth_text_name + amth_text.write("# Amaranth Startup Script\nimport bpy\n") + amth_text.use_module = True + + return amth_text_exists + except AttributeError: + return None + + +# FEATURE: Dupli Group Path +def ui_dupli_group_library_path(self, context): + + ob = context.object + + row = self.layout.row() + row.alignment = "LEFT" + + if ob and ob.instance_collection and ob.instance_collection.library: + lib = ob.instance_collection.library.filepath + + row.operator(AMTH_SCENE_OT_blender_instance_open.bl_idname, + text="Library: %s" % lib, + emboss=False, + icon="LINK_BLEND").filepath = lib +# // FEATURE: Dupli Group Path + + +# FEATURE: Object ID for objects inside DupliGroups +class AMTH_OBJECT_OT_id_dupligroup(bpy.types.Operator): + + """Set the Object ID for objects in the dupli group""" + bl_idname = "object.amaranth_object_id_duplis" + bl_label = "Apply Object ID to Duplis" + + clear = False + + @classmethod + def poll(cls, context): + return context.active_object.instance_collection + + def execute(self, context): + self.__class__.clear = False + ob = context.active_object + amaranth_text_startup(context) + script_exists = False + script_intro = "# OB ID: %s" % ob.name + obdata = 'bpy.data.objects[" % s"]' % ob.name + # TODO: cleanup script var using format or template strings + script = "%s" % ( + "\nif %(obdata)s and %(obdata)s.instance_collection and %(obdata)s.pass_index != 0: %(obname)s \n" + " for dob in %(obdata)s.instance_collection.objects: %(obname)s \n" + " dob.pass_index = %(obdata)s.pass_index %(obname)s \n" % + {"obdata": obdata, "obname": script_intro}) + + for txt in bpy.data.texts: + if txt.name == amth_text.name: + for li in txt.lines: + if script_intro == li.body: + script_exists = True + continue + + if not script_exists: + amth_text.write("\n") + amth_text.write(script_intro) + amth_text.write(script) + + if ob and ob.instance_collection: + if ob.pass_index != 0: + for dob in ob.instance_collection.objects: + dob.pass_index = ob.pass_index + + self.report({"INFO"}, + "%s ID: %s to all objects in this Dupli Group" % ( + "Applied" if not script_exists else "Updated", + ob.pass_index)) + + return {"FINISHED"} + + +class AMTH_OBJECT_OT_id_dupligroup_clear(bpy.types.Operator): + + """Clear the Object ID from objects in dupli group""" + bl_idname = "object.amaranth_object_id_duplis_clear" + bl_label = "Clear Object ID from Duplis" + + @classmethod + def poll(cls, context): + return context.active_object.instance_collection + + def execute(self, context): + context.active_object.pass_index = 0 + AMTH_OBJECT_OT_id_dupligroup.clear = True + amth_text_exists = amaranth_text_startup(context) + match_first = "# OB ID: %s" % context.active_object.name + + if amth_text_exists: + for txt in bpy.data.texts: + if txt.name == amth_text.name: + for li in txt.lines: + if match_first in li.body: + li.body = "" + continue + + self.report({"INFO"}, "Object IDs back to normal") + return {"FINISHED"} + + +def ui_object_id_duplis(self, context): + + if context.active_object.instance_collection: + split = self.layout.split() + row = split.row(align=True) + row.enabled = context.active_object.pass_index != 0 + row.operator( + AMTH_OBJECT_OT_id_dupligroup.bl_idname) + row.operator( + AMTH_OBJECT_OT_id_dupligroup_clear.bl_idname, + icon="X", text="") + split.separator() + + if AMTH_OBJECT_OT_id_dupligroup.clear: + self.layout.label(text="Next time you save/reload this file, " + "object IDs will be back to normal", + icon="INFO") +# // FEATURE: Object ID for objects inside DupliGroups + + +def register(): + bpy.utils.register_class(AMTH_OBJECT_OT_id_dupligroup) + bpy.utils.register_class(AMTH_OBJECT_OT_id_dupligroup_clear) + bpy.types.OBJECT_PT_instancing.append(ui_dupli_group_library_path) + bpy.types.OBJECT_PT_relations.append(ui_object_id_duplis) + + +def unregister(): + bpy.utils.unregister_class(AMTH_OBJECT_OT_id_dupligroup) + bpy.utils.unregister_class(AMTH_OBJECT_OT_id_dupligroup_clear) + bpy.types.OBJECT_PT_instancing.remove(ui_dupli_group_library_path) + bpy.types.OBJECT_PT_relations.remove(ui_object_id_duplis) diff --git a/amaranth/misc/sequencer_extra_info.py b/amaranth/misc/sequencer_extra_info.py new file mode 100644 index 00000000..2d5d6b79 --- /dev/null +++ b/amaranth/misc/sequencer_extra_info.py @@ -0,0 +1,67 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Sequencer: Display Image File Name + +When seeking through an image sequence, display the active strips' file name +for the current frame, and it's [playhead]. + +Find it on the VSE header. +""" +import bpy + + +# FEATURE: Sequencer Extra Info +def act_strip(context): + try: + return context.scene.sequence_editor.active_strip + except AttributeError: + return None + + +def ui_sequencer_extra_info(self, context): + layout = self.layout + strip = act_strip(context) + if strip: + seq_type = strip.type + if seq_type and seq_type == 'IMAGE': + elem = strip.strip_elem_from_frame(context.scene.frame_current) + if elem: + layout.label( + text="%s %s" % + (elem.filename, "[%s]" % + (context.scene.frame_current - strip.frame_start))) + +# // FEATURE: Sequencer Extra Info + + +def register(): + bpy.types.SEQUENCER_HT_header.append(ui_sequencer_extra_info) + + +def unregister(): + bpy.types.SEQUENCER_HT_header.remove(ui_sequencer_extra_info) diff --git a/amaranth/misc/toggle_wire.py b/amaranth/misc/toggle_wire.py new file mode 100644 index 00000000..3d43660d --- /dev/null +++ b/amaranth/misc/toggle_wire.py @@ -0,0 +1,134 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import bpy + + +# FEATURE: Toggle Wire Display +class AMTH_OBJECT_OT_wire_toggle(bpy.types.Operator): + + """Turn on/off wire display on mesh objects""" + bl_idname = "object.amth_wire_toggle" + bl_label = "Display Wireframe" + bl_options = {"REGISTER", "UNDO"} + + clear: bpy.props.BoolProperty( + default=False, name="Clear Wireframe", + description="Clear Wireframe Display") + + def execute(self, context): + + scene = context.scene + is_all_scenes = scene.amth_wire_toggle_scene_all + is_selected = scene.amth_wire_toggle_is_selected + is_all_edges = scene.amth_wire_toggle_edges_all + is_optimal = scene.amth_wire_toggle_optimal + clear = self.clear + + if is_all_scenes: + which = bpy.data.objects + elif is_selected: + if not context.selected_objects: + self.report({"INFO"}, "No selected objects") + which = context.selected_objects + else: + which = scene.objects + + if which: + for ob in which: + if ob and ob.type in { + "MESH", "EMPTY", "CURVE", + "META", "SURFACE", "FONT"}: + + ob.show_wire = False if clear else True + ob.show_all_edges = is_all_edges + + for mo in ob.modifiers: + if mo and mo.type == "SUBSURF": + mo.show_only_control_edges = is_optimal + + return {"FINISHED"} + + +def ui_object_wire_toggle(self, context): + + scene = context.scene + + self.layout.separator() + col = self.layout.column(align=True) + col.label(text="Wireframes:") + row = col.row(align=True) + row.operator(AMTH_OBJECT_OT_wire_toggle.bl_idname, + icon="MOD_WIREFRAME", text="Display").clear = False + row.operator(AMTH_OBJECT_OT_wire_toggle.bl_idname, + icon="X", text="Clear").clear = True + col.separator() + row = col.row(align=True) + row.prop(scene, "amth_wire_toggle_edges_all") + row.prop(scene, "amth_wire_toggle_optimal") + row = col.row(align=True) + sub = row.row(align=True) + sub.active = not scene.amth_wire_toggle_scene_all + sub.prop(scene, "amth_wire_toggle_is_selected") + sub = row.row(align=True) + sub.active = not scene.amth_wire_toggle_is_selected + sub.prop(scene, "amth_wire_toggle_scene_all") + + +def init_properties(): + scene = bpy.types.Scene + scene.amth_wire_toggle_scene_all = bpy.props.BoolProperty( + default=False, + name="All Scenes", + description="Toggle wire on objects in all scenes") + scene.amth_wire_toggle_is_selected = bpy.props.BoolProperty( + default=False, + name="Only Selected Objects", + description="Only toggle wire on selected objects") + scene.amth_wire_toggle_edges_all = bpy.props.BoolProperty( + default=True, + name="Draw All Edges", + description="Draw all the edges even on coplanar faces") + scene.amth_wire_toggle_optimal = bpy.props.BoolProperty( + default=False, + name="Subsurf Optimal Display", + description="Skip drawing/rendering of interior subdivided edges " + "on meshes with Subdivision Surface modifier") + + +def clear_properties(): + props = ( + 'amth_wire_toggle_is_selected', + 'amth_wire_toggle_scene_all', + "amth_wire_toggle_edges_all", + "amth_wire_toggle_optimal" + ) + wm = bpy.context.window_manager + for p in props: + if p in wm: + del wm[p] + +# //FEATURE: Toggle Wire Display + + +def register(): + init_properties() + bpy.utils.register_class(AMTH_OBJECT_OT_wire_toggle) + bpy.types.VIEW3D_PT_view3d_properties.append(ui_object_wire_toggle) + + +def unregister(): + bpy.utils.unregister_class(AMTH_OBJECT_OT_wire_toggle) + bpy.types.VIEW3D_PT_view3d_properties.remove(ui_object_wire_toggle) + clear_properties() diff --git a/amaranth/modeling/__init__.py b/amaranth/modeling/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/amaranth/modeling/__init__.py diff --git a/amaranth/modeling/symmetry_tools.py b/amaranth/modeling/symmetry_tools.py new file mode 100644 index 00000000..da342abc --- /dev/null +++ b/amaranth/modeling/symmetry_tools.py @@ -0,0 +1,189 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Symmetry Tools: Find Asymmetric + Make Symmetric (by Sergey Sharybin) + +Our character wasn’t completely symmetric in some parts where it was +supposed to, this could be by moving vertices by mistake or just reasons. +To fix this in a fast way, Sergey coded this two super useful tools: + +* Find Asymmetric: +Selects vertices that don’t have the same position on the opposite side. + +* Make Symmetric: +Move selected vertices to match the position of those on the other side. + +This tools may not apply on every single model out there, but I tried it +in many different characters and it worked. So probably better use it on +those models that were already symmetric at some point, modeled with a +mirror modifier or so. +Search (spacebar) for "Find Asymmetric", and "Make Symmetric""Settings". + +> Developed during Caminandes Open Movie Project +""" + +import bpy +import bmesh +from mathutils import Vector + + +class AMTH_MESH_OT_find_asymmetric(bpy.types.Operator): + + """ + Find asymmetric vertices + """ + + bl_idname = "mesh.find_asymmetric" + bl_label = "Find Asymmetric" + bl_options = {"UNDO", "REGISTER"} + + @classmethod + def poll(cls, context): + object = context.object + if object: + return object.mode == "EDIT" and object.type == "MESH" + return False + + def execute(self, context): + threshold = 1e-6 + + object = context.object + bm = bmesh.from_edit_mesh(object.data) + + # Deselect all the vertices + for v in bm.verts: + v.select = False + + for v1 in bm.verts: + if abs(v1.co[0]) < threshold: + continue + + mirror_found = False + for v2 in bm.verts: + if v1 == v2: + continue + if v1.co[0] * v2.co[0] > 0.0: + continue + + mirror_coord = Vector(v2.co) + mirror_coord[0] *= -1 + if (mirror_coord - v1.co).length_squared < threshold: + mirror_found = True + break + if not mirror_found: + v1.select = True + + bm.select_flush_mode() + + bmesh.update_edit_mesh(object.data) + + return {"FINISHED"} + + +class AMTH_MESH_OT_make_symmetric(bpy.types.Operator): + + """ + Make symmetric + """ + + bl_idname = "mesh.make_symmetric" + bl_label = "Make Symmetric" + bl_options = {"UNDO", "REGISTER"} + + @classmethod + def poll(cls, context): + object = context.object + if object: + return object.mode == "EDIT" and object.type == "MESH" + return False + + def execute(self, context): + threshold = 1e-6 + + object = context.object + bm = bmesh.from_edit_mesh(object.data) + + for v1 in bm.verts: + if v1.co[0] < threshold: + continue + if not v1.select: + continue + + closest_vert = None + closest_distance = -1 + for v2 in bm.verts: + if v1 == v2: + continue + if v2.co[0] > threshold: + continue + if not v2.select: + continue + + mirror_coord = Vector(v2.co) + mirror_coord[0] *= -1 + distance = (mirror_coord - v1.co).length_squared + if closest_vert is None or distance < closest_distance: + closest_distance = distance + closest_vert = v2 + + if closest_vert: + closest_vert.select = False + closest_vert.co = Vector(v1.co) + closest_vert.co[0] *= -1 + v1.select = False + + for v1 in bm.verts: + if v1.select: + closest_vert = None + closest_distance = -1 + for v2 in bm.verts: + if v1 != v2: + mirror_coord = Vector(v2.co) + mirror_coord[0] *= -1 + distance = (mirror_coord - v1.co).length_squared + if closest_vert is None or distance < closest_distance: + closest_distance = distance + closest_vert = v2 + if closest_vert: + v1.select = False + v1.co = Vector(closest_vert.co) + v1.co[0] *= -1 + + bm.select_flush_mode() + bmesh.update_edit_mesh(object.data) + + return {"FINISHED"} + + +def ui_symmetry_tools(self, context): + if bpy.context.mode == 'EDIT_MESH': + self.layout.separator() + self.layout.operator( + AMTH_MESH_OT_find_asymmetric.bl_idname, + icon="ALIGN_CENTER", text="Find Asymmetric") + self.layout.operator( + AMTH_MESH_OT_make_symmetric.bl_idname, + icon="ALIGN_JUSTIFY", text="Make Symmetric") + + +def register(): + bpy.utils.register_class(AMTH_MESH_OT_find_asymmetric) + bpy.utils.register_class(AMTH_MESH_OT_make_symmetric) + bpy.types.VIEW3D_MT_edit_mesh.append(ui_symmetry_tools) + + +def unregister(): + bpy.utils.unregister_class(AMTH_MESH_OT_find_asymmetric) + bpy.utils.unregister_class(AMTH_MESH_OT_make_symmetric) + bpy.types.VIEW3D_MT_edit_mesh.remove(ui_symmetry_tools) diff --git a/amaranth/node_editor/__init__.py b/amaranth/node_editor/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/amaranth/node_editor/__init__.py diff --git a/amaranth/node_editor/display_image.py b/amaranth/node_editor/display_image.py new file mode 100644 index 00000000..0dcefca8 --- /dev/null +++ b/amaranth/node_editor/display_image.py @@ -0,0 +1,105 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Display Active Image Node on Image Editor + +When selecting an Image node, it will show it on the Image editor (if +there is any available). If you don't like this behavior, you can +disable it from the Amaranth Toolset panel on the Scene properties. +Coded by the awesome Sergey Sharybin. This feature only works on Blender +2.68 and newer. Select an Image Node in the Compositor or Cycles nodes +editor, there must be at least one image editor available. +""" + +import bpy + + +KEYMAPS = list() + +image_nodes = ("CompositorNodeRLayers", + "CompositorNodeImage", + "CompositorNodeViewer", + "CompositorNodeComposite", + "ShaderNodeTexImage", + "ShaderNodeTexEnvironment") + + +class AMTH_NODE_OT_show_active_node_image(bpy.types.Operator): + """Show active image node image in the image editor""" + bl_idname = "node.show_active_node_image" + bl_label = "Show Active Node Node" + bl_options = {"UNDO"} + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + mlocx = event.mouse_region_x + mlocy = event.mouse_region_y + select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False) + + if 'FINISHED' in select_node: # Only run if we're clicking on a node + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return {"CANCELLED"} + + preferences = context.preferences.addons["amaranth"].preferences + if preferences.use_image_node_display: + if context.active_node: + active_node = context.active_node + + if active_node.bl_idname in image_nodes: + # Use largest image editor + area = None + area_size = 0 + for a in context.screen.areas: + if a.type == "IMAGE_EDITOR": + size = a.width * a.height + if size > area_size: + area_size = size + area = a + if area: + for space in area.spaces: + if space.type == "IMAGE_EDITOR": + if active_node.bl_idname == "CompositorNodeViewer": + space.image = bpy.data.images[ + "Viewer Node"] + elif active_node.bl_idname in ["CompositorNodeComposite", "CompositorNodeRLayers"]: + space.image = bpy.data.images[ + "Render Result"] + elif active_node.image: + space.image = active_node.image + break + else: + return {'CANCELLED'} + + return {"FINISHED"} + else: + return {"PASS_THROUGH"} + + +def register(): + bpy.utils.register_class(AMTH_NODE_OT_show_active_node_image) + kc = bpy.context.window_manager.keyconfigs.addon + km = kc.keymaps.new(name="Node Editor", space_type="NODE_EDITOR") + kmi = km.keymap_items.new("node.show_active_node_image", + "LEFTMOUSE", "DOUBLE_CLICK") + KEYMAPS.append((km, kmi)) + + +def unregister(): + bpy.utils.unregister_class(AMTH_NODE_OT_show_active_node_image) + for km, kmi in KEYMAPS: + km.keymap_items.remove(kmi) + KEYMAPS.clear() diff --git a/amaranth/node_editor/id_panel.py b/amaranth/node_editor/id_panel.py new file mode 100644 index 00000000..09f031da --- /dev/null +++ b/amaranth/node_editor/id_panel.py @@ -0,0 +1,154 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +""" +Object / Material Indices Panel + +When working with ID Masks in the Nodes Editor, is hard to follow track +of which objects/materials have which ID. +This adds a panel on the sidebar when an ID Mask node is selected. +The active object is highlighted between [square brackets] On the Nodes +Editor's sidebar, when an ID Mask node is selected. +""" + +import bpy + + +class AMTH_NODE_PT_indices(bpy.types.Panel): + bl_space_type = "NODE_EDITOR" + bl_region_type = "UI" + bl_label = "Object / Material Indices" + bl_options = {"DEFAULT_CLOSED"} + + @classmethod + def poll(cls, context): + node = context.active_node + return node and node.type == "ID_MASK" + + def draw(self, context): + layout = self.layout + + objects = bpy.data.objects + materials = bpy.data.materials + node = context.active_node + + show_ob_id = False + show_ma_id = False + matching_ids = False + + if context.active_object: + ob_act = context.active_object + else: + ob_act = False + + for ob in objects: + if ob and ob.pass_index > 0: + show_ob_id = True + for ma in materials: + if ma and ma.pass_index > 0: + show_ma_id = True + row = layout.row(align=True) + row.prop(node, "index", text="Mask Index") + row.prop(node, "use_matching_indices", text="Only Matching IDs") + + layout.separator() + + if not show_ob_id and not show_ma_id: + layout.label( + text="No objects or materials indices so far.", icon="INFO") + + if show_ob_id: + split = layout.split() + col = split.column() + col.label(text="Object Name") + split.label(text="ID Number") + row = layout.row() + for ob in objects: + icon = "OUTLINER_DATA_" + ob.type + if ob.library: + icon = "LIBRARY_DATA_DIRECT" + elif ob.is_library_indirect: + icon = "LIBRARY_DATA_INDIRECT" + + if ob and node.use_matching_indices \ + and ob.pass_index == node.index \ + and ob.pass_index != 0: + matching_ids = True + row.label( + text="[{}]".format(ob.name) + if ob_act and ob.name == ob_act.name else ob.name, + icon=icon) + row.label(text="%s" % ob.pass_index) + row = layout.row() + + elif ob and not node.use_matching_indices \ + and ob.pass_index > 0: + + matching_ids = True + row.label( + text="[{}]".format(ob.name) + if ob_act and ob.name == ob_act.name else ob.name, + icon=icon) + row.label(text="%s" % ob.pass_index) + row = layout.row() + + if node.use_matching_indices and not matching_ids: + row.label(text="No objects with ID %s" % + node.index, icon="INFO") + + layout.separator() + + if show_ma_id: + split = layout.split() + col = split.column() + col.label(text="Material Name") + split.label(text="ID Number") + row = layout.row() + + for ma in materials: + icon = "BLANK1" + if ma.use_nodes: + icon = "NODETREE" + elif ma.library: + icon = "LIBRARY_DATA_DIRECT" + if ma.is_library_indirect: + icon = "LIBRARY_DATA_INDIRECT" + + if ma and node.use_matching_indices \ + and ma.pass_index == node.index \ + and ma.pass_index != 0: + matching_ids = True + row.label(text="%s" % ma.name, icon=icon) + row.label(text="%s" % ma.pass_index) + row = layout.row() + + elif ma and not node.use_matching_indices \ + and ma.pass_index > 0: + + matching_ids = True + row.label(text="%s" % ma.name, icon=icon) + row.label(text="%s" % ma.pass_index) + row = layout.row() + + if node.use_matching_indices and not matching_ids: + row.label(text="No materials with ID %s" % + node.index, icon="INFO") + + +def register(): + bpy.utils.register_class(AMTH_NODE_PT_indices) + + +def unregister(): + bpy.utils.unregister_class(AMTH_NODE_PT_indices) diff --git a/amaranth/node_editor/node_shader_extra.py b/amaranth/node_editor/node_shader_extra.py new file mode 100644 index 00000000..7fbaf225 --- /dev/null +++ b/amaranth/node_editor/node_shader_extra.py @@ -0,0 +1,40 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +import bpy + + +# FEATURE: Shader Nodes Extra Info +def node_shader_extra(self, context): + if context.space_data.tree_type == 'ShaderNodeTree': + ob = context.active_object + snode = context.space_data + layout = self.layout + + if ob and snode.shader_type == 'OBJECT': + if ob.type == 'LAMP': + layout.label(text="%s" % ob.name, + icon="LAMP_%s" % ob.data.type) + else: + layout.label(text="%s" % ob.name, + icon="OUTLINER_DATA_%s" % ob.type) + +# // FEATURE: Shader Nodes Extra Info + + +def register(): + bpy.types.NODE_HT_header.append(node_shader_extra) + + +def unregister(): + bpy.types.NODE_HT_header.remove(node_shader_extra) diff --git a/amaranth/node_editor/node_stats.py b/amaranth/node_editor/node_stats.py new file mode 100644 index 00000000..796e10dc --- /dev/null +++ b/amaranth/node_editor/node_stats.py @@ -0,0 +1,45 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Nodes Stats + +Display the number of selected and total nodes on the compositor. On the +Compositing Nodes Editor. +""" + +import bpy + + +def node_stats(self, context): + if context.scene.node_tree: + tree_type = context.space_data.tree_type + nodes = context.scene.node_tree.nodes + nodes_total = len(nodes.keys()) + nodes_selected = 0 + for n in nodes: + if n.select: + nodes_selected = nodes_selected + 1 + + if tree_type == 'CompositorNodeTree': + layout = self.layout + row = layout.row(align=True) + row.label(text="Nodes: %s/%s" % (nodes_selected, str(nodes_total))) + + +def register(): + bpy.types.NODE_HT_header.append(node_stats) + + +def unregister(): + bpy.types.NODE_HT_header.remove(node_stats) diff --git a/amaranth/node_editor/normal_node.py b/amaranth/node_editor/normal_node.py new file mode 100644 index 00000000..fe77856a --- /dev/null +++ b/amaranth/node_editor/normal_node.py @@ -0,0 +1,83 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Nodes: XYZ Sliders for Normal Node + +Tweak the Normal node more accurately by using these sliders. Not the most +intuitive way to tweak, but it helps. + +ProTip: Hit Shift+Drag for moving in very small steps. + +Coded by Lukas Töenne. Thanks! +Find it on the Properties panel, when selecting a Normal node. +""" + + +import bpy +from mathutils import Vector + + +# FEATURE: Normal Node Values, by Lukas Tönne +def init(): + prop_normal_vector = bpy.props.FloatVectorProperty( + name="Normal", size=3, subtype='XYZ', + min=-1.0, max=1.0, soft_min=-1.0, soft_max=1.0, + get=normal_vector_get, set=normal_vector_set + ) + bpy.types.ShaderNodeNormal.normal_vector = prop_normal_vector + bpy.types.CompositorNodeNormal.normal_vector = prop_normal_vector + + +def clear(): + del bpy.types.ShaderNodeNormal.normal_vector + del bpy.types.CompositorNodeNormal.normal_vector + + +def normal_vector_get(self): + return self.outputs['Normal'].default_value + + +def normal_vector_set(self, values): + # default_value allows un-normalized values, + # do this here to prevent awkward results + values = Vector(values).normalized() + self.outputs['Normal'].default_value = values + + +def act_node(context): + try: + return context.active_node + except AttributeError: + return None + + +def ui_node_normal_values(self, context): + + node = act_node(context) + + if act_node: + if node and node.type == 'NORMAL': + self.layout.prop(node, "normal_vector", text="") + +# // FEATURE: Normal Node Values, by Lukas Tönne + + +def register(): + init() + bpy.types.NODE_PT_active_node_properties.append(ui_node_normal_values) + + +def unregister(): + bpy.types.NODE_PT_active_node_properties.remove(ui_node_normal_values) + clear() diff --git a/amaranth/node_editor/simplify_nodes.py b/amaranth/node_editor/simplify_nodes.py new file mode 100644 index 00000000..71aec24c --- /dev/null +++ b/amaranth/node_editor/simplify_nodes.py @@ -0,0 +1,147 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Nodes Simplify Panel [WIP Feature] + +Disable/Enable certain nodes at a time. Useful to quickly "simplify" +compositing. +This feature is a work in progress, the main issue now is when switching +many different kinds one after the other. + +On the Nodes Editor Properties N panel. +""" + +import bpy + + +def init(): + nodes_compo_types = ( + ("ALL", "All Types", "", 0), + ("BLUR", "Blur", "", 1), + ("BOKEHBLUR", "Bokeh Blur", "", 2), + ("VECBLUR", "Vector Blur", "", 3), + ("DEFOCUS", "Defocus", "", 4), + ("R_LAYERS", "Render Layer", "", 5), + ) + node = bpy.types.Node + nodes_compo = bpy.types.CompositorNodeTree + nodes_compo.types = bpy.props.EnumProperty( + items=nodes_compo_types, name="Types") + nodes_compo.toggle_mute = bpy.props.BoolProperty(default=False) + node.status = bpy.props.BoolProperty(default=False) + + +def clear(): + wm = bpy.context.window_manager + for p in ("types", "toggle_mute", "status"): + if wm.get(p): + del wm[p] + + +class AMTH_NODE_PT_simplify(bpy.types.Panel): + + bl_space_type = "NODE_EDITOR" + bl_region_type = "UI" + bl_label = "Simplify" + bl_options = {"DEFAULT_CLOSED"} + + @classmethod + def poll(cls, context): + space = context.space_data + return space.type == "NODE_EDITOR" \ + and space.node_tree is not None \ + and space.tree_type == "CompositorNodeTree" + + def draw(self, context): + layout = self.layout + node_tree = context.scene.node_tree + + if node_tree is not None: + layout.prop(node_tree, "types") + layout.operator(AMTH_NODE_OT_toggle_mute.bl_idname, + text="Turn On" if node_tree.toggle_mute else "Turn Off", + icon="RESTRICT_VIEW_OFF" if node_tree.toggle_mute else "RESTRICT_VIEW_ON") + + if node_tree.types == "VECBLUR": + layout.label(text="This will also toggle the Vector pass {}".format( + "on" if node_tree.toggle_mute else "off"), icon="INFO") + + +class AMTH_NODE_OT_toggle_mute(bpy.types.Operator): + + bl_idname = "node.toggle_mute" + bl_label = "Toggle Mute" + + def execute(self, context): + scene = context.scene + node_tree = scene.node_tree + node_type = node_tree.types + rlayers = scene.render + + if "amaranth_pass_vector" not in scene.keys(): + scene["amaranth_pass_vector"] = [] + + # can"t extend() the list, so make a dummy one + pass_vector = scene["amaranth_pass_vector"] + + if not pass_vector: + pass_vector = [] + + if node_tree.toggle_mute: + for node in node_tree.nodes: + if node_type == "ALL": + node.mute = node.status + if node.type == node_type: + node.mute = node.status + if node_type == "VECBLUR": + for layer in rlayers.layers: + if layer.name in pass_vector: + layer.use_pass_vector = True + pass_vector.remove(layer.name) + + node_tree.toggle_mute = False + + else: + for node in node_tree.nodes: + if node_type == "ALL": + node.mute = True + if node.type == node_type: + node.status = node.mute + node.mute = True + if node_type == "VECBLUR": + for layer in rlayers.layers: + if layer.use_pass_vector: + pass_vector.append(layer.name) + layer.use_pass_vector = False + pass + + node_tree.toggle_mute = True + + # Write back to the custom prop + pass_vector = sorted(set(pass_vector)) + scene["amaranth_pass_vector"] = pass_vector + + return {"FINISHED"} + + +def register(): + init() + bpy.utils.register_class(AMTH_NODE_PT_simplify) + bpy.utils.register_class(AMTH_NODE_OT_toggle_mute) + + +def unregister(): + clear() + bpy.utils.unregister_class(AMTH_NODE_PT_simplify) + bpy.utils.unregister_class(AMTH_NODE_OT_toggle_mute) diff --git a/amaranth/node_editor/switch_material.py b/amaranth/node_editor/switch_material.py new file mode 100644 index 00000000..d559965b --- /dev/null +++ b/amaranth/node_editor/switch_material.py @@ -0,0 +1,68 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Material Selector + +Quickly switch materials in the active mesh without going to the Properties editor + +Based on 'Afeitadora's work on Elysiun +http://www.elysiun.com/forum/showthread.php?290097-Dynamic-Object-Dropdown-List&p=2361851#post2361851 + +""" + +import bpy + +def ui_node_editor_material_select(self, context): + + act_ob = context.active_object + + if act_ob and context.active_object.type in {'MESH', 'CURVE', 'SURFACE', 'META'} and \ + context.space_data.tree_type == 'ShaderNodeTree' and \ + context.space_data.shader_type == 'OBJECT': + + if act_ob.active_material: + mat_name = act_ob.active_material.name + else: + mat_name = "No Material" + + self.layout.operator_menu_enum("material.menu_select", + "material_select", + text=mat_name, + icon="MATERIAL") + +class AMNodeEditorMaterialSelect(bpy.types.Operator): + bl_idname = "material.menu_select" + bl_label = "Select Material" + bl_description = "Switch to another material in this mesh" + + def avail_materials(self,context): + items = [(str(i),x.name,x.name, "MATERIAL", i) for i,x in enumerate(bpy.context.active_object.material_slots)] + return items + material_select: bpy.props.EnumProperty(items = avail_materials, name = "Available Materials") + + @classmethod + def poll(cls, context): + return context.active_object + + def execute(self,context): + bpy.context.active_object.active_material_index = int(self.material_select) + return {'FINISHED'} + +def register(): + bpy.utils.register_class(AMNodeEditorMaterialSelect) + bpy.types.NODE_HT_header.append(ui_node_editor_material_select) + +def unregister(): + bpy.utils.unregister_class(AMNodeEditorMaterialSelect) + bpy.types.NODE_HT_header.remove(ui_node_editor_material_select) diff --git a/amaranth/node_editor/templates/__init__.py b/amaranth/node_editor/templates/__init__.py new file mode 100644 index 00000000..90edac48 --- /dev/null +++ b/amaranth/node_editor/templates/__init__.py @@ -0,0 +1,81 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Node Templates - Vignette, Vector Blur + +Add a set of nodes with one click, in this version I added a "Vignette" +as first example. + +There is no official way to make a vignette, this is just my approach at +it. Templates: On the Compositor's header, "Template" pulldown. Or hit W. +Vignette: Adjust the size and position of the vignette with the Ellipse +Mask's X/Y and width, height values. +""" + +import bpy +from amaranth.node_editor.templates.vectorblur import AMTH_NODE_OT_AddTemplateVectorBlur +from amaranth.node_editor.templates.vignette import AMTH_NODE_OT_AddTemplateVignette + + +KEYMAPS = list() + + +# Node Templates Menu +class AMTH_NODE_MT_amaranth_templates(bpy.types.Menu): + bl_idname = 'AMTH_NODE_MT_amaranth_templates' + bl_space_type = 'NODE_EDITOR' + bl_label = "Templates" + bl_description = "List of Amaranth Templates" + + def draw(self, context): + layout = self.layout + layout.operator( + AMTH_NODE_OT_AddTemplateVectorBlur.bl_idname, + text="Vector Blur", + icon='FORCE_HARMONIC') + layout.operator( + AMTH_NODE_OT_AddTemplateVignette.bl_idname, + text="Vignette", + icon='COLOR') + + +def node_templates_pulldown(self, context): + if context.space_data.tree_type == 'CompositorNodeTree': + layout = self.layout + row = layout.row(align=True) + row.scale_x = 1.3 + row.menu("AMTH_NODE_MT_amaranth_templates", + icon="NODETREE") + + +def register(): + bpy.utils.register_class(AMTH_NODE_MT_amaranth_templates) + bpy.utils.register_class(AMTH_NODE_OT_AddTemplateVignette) + bpy.utils.register_class(AMTH_NODE_OT_AddTemplateVectorBlur) + bpy.types.NODE_HT_header.append(node_templates_pulldown) + kc = bpy.context.window_manager.keyconfigs.addon + km = kc.keymaps.new(name="Node Editor", space_type="NODE_EDITOR") + kmi = km.keymap_items.new("wm.call_menu", "W", "PRESS") + kmi.properties.name = "AMTH_NODE_MT_amaranth_templates" + KEYMAPS.append((km, kmi)) + + +def unregister(): + bpy.utils.unregister_class(AMTH_NODE_MT_amaranth_templates) + bpy.utils.unregister_class(AMTH_NODE_OT_AddTemplateVignette) + bpy.utils.unregister_class(AMTH_NODE_OT_AddTemplateVectorBlur) + bpy.types.NODE_HT_header.remove(node_templates_pulldown) + for km, kmi in KEYMAPS: + km.keymap_items.remove(kmi) + KEYMAPS.clear() diff --git a/amaranth/node_editor/templates/vectorblur.py b/amaranth/node_editor/templates/vectorblur.py new file mode 100644 index 00000000..2bc01e48 --- /dev/null +++ b/amaranth/node_editor/templates/vectorblur.py @@ -0,0 +1,69 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import bpy +from mathutils import Vector + + +class AMTH_NODE_OT_AddTemplateVectorBlur(bpy.types.Operator): + bl_idname = "node.template_add_vectorblur" + bl_label = "Add Vector Blur" + bl_description = "Add a vector blur filter" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + space = context.space_data + tree = context.scene.node_tree + return space.type == "NODE_EDITOR" \ + and space.node_tree is not None \ + and space.tree_type == "CompositorNodeTree" \ + and tree \ + and tree.nodes.active \ + and tree.nodes.active.type == "R_LAYERS" + + def _setupNodes(self, context): + scene = context.scene + space = context.space_data + tree = scene.node_tree + + bpy.ops.node.select_all(action="DESELECT") + + act_node = tree.nodes.active + #rlayer = act_node.scene.render.layers[act_node.layer] + + #if not rlayer.use_pass_vector: + #rlayer.use_pass_vector = True + + vblur = tree.nodes.new(type="CompositorNodeVecBlur") + vblur.use_curved = True + vblur.factor = 0.5 + + tree.links.new(act_node.outputs["Image"], vblur.inputs["Image"]) + tree.links.new(act_node.outputs["Depth"], vblur.inputs["Z"]) + tree.links.new(act_node.outputs["Vector"], vblur.inputs["Speed"]) + + if tree.nodes.active: + vblur.location = tree.nodes.active.location + vblur.location += Vector((250.0, 0.0)) + else: + vblur.location += Vector( + (space.cursor_location[0], space.cursor_location[1])) + + vblur.select = True + + def execute(self, context): + self._setupNodes(context) + + return {"FINISHED"} diff --git a/amaranth/node_editor/templates/vignette.py b/amaranth/node_editor/templates/vignette.py new file mode 100644 index 00000000..5e8ce0a4 --- /dev/null +++ b/amaranth/node_editor/templates/vignette.py @@ -0,0 +1,100 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import bpy +from mathutils import Vector + + +class AMTH_NODE_OT_AddTemplateVignette(bpy.types.Operator): + bl_idname = "node.template_add_vignette" + bl_label = "Add Vignette" + bl_description = "Add a vignette effect" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + space = context.space_data + return space.type == "NODE_EDITOR" \ + and space.node_tree is not None \ + and space.tree_type == "CompositorNodeTree" + + # used as reference the setup scene script from master nazgul + def _setupNodes(self, context): + scene = context.scene + space = context.space_data + tree = scene.node_tree + has_act = True if tree.nodes.active else False + + bpy.ops.node.select_all(action="DESELECT") + + ellipse = tree.nodes.new(type="CompositorNodeEllipseMask") + ellipse.width = 0.8 + ellipse.height = 0.4 + blur = tree.nodes.new(type="CompositorNodeBlur") + blur.use_relative = True + blur.factor_x = 30 + blur.factor_y = 50 + ramp = tree.nodes.new(type="CompositorNodeValToRGB") + ramp.color_ramp.interpolation = "B_SPLINE" + ramp.color_ramp.elements[1].color = (0.6, 0.6, 0.6, 1) + + overlay = tree.nodes.new(type="CompositorNodeMixRGB") + overlay.blend_type = "OVERLAY" + overlay.inputs[0].default_value = 0.8 + overlay.inputs[1].default_value = (0.5, 0.5, 0.5, 1) + + tree.links.new(ellipse.outputs["Mask"], blur.inputs["Image"]) + tree.links.new(blur.outputs["Image"], ramp.inputs[0]) + tree.links.new(ramp.outputs["Image"], overlay.inputs[2]) + if has_act: + tree.links.new(tree.nodes.active.outputs[0], overlay.inputs[1]) + + if has_act: + overlay.location = tree.nodes.active.location + overlay.location += Vector((350.0, 0.0)) + else: + overlay.location += Vector( + (space.cursor_location[0], space.cursor_location[1])) + + ellipse.location = overlay.location + ellipse.location += Vector((-715.0, -400)) + ellipse.inputs[0].hide = True + ellipse.inputs[1].hide = True + + blur.location = ellipse.location + blur.location += Vector((300.0, 0.0)) + blur.inputs["Size"].hide = True + + ramp.location = blur.location + ramp.location += Vector((175.0, 0)) + ramp.outputs["Alpha"].hide = True + + for node in (ellipse, blur, ramp, overlay): + node.select = True + node.show_preview = False + + bpy.ops.node.join() + + frame = ellipse.parent + frame.label = "Vignette" + frame.use_custom_color = True + frame.color = (0.1, 0.1, 0.1) + + overlay.parent = None + overlay.label = "Vignette Overlay" + + def execute(self, context): + self._setupNodes(context) + + return {"FINISHED"} diff --git a/amaranth/prefs.py b/amaranth/prefs.py new file mode 100644 index 00000000..3c27c12d --- /dev/null +++ b/amaranth/prefs.py @@ -0,0 +1,134 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import bpy +from bpy.props import ( + BoolProperty, + IntProperty, + ) + + +class AmaranthToolsetPreferences(bpy.types.AddonPreferences): + bl_idname = "amaranth" + use_frame_current: BoolProperty( + name="Current Frame Slider", + description="Set the current frame from the Specials menu in the 3D View", + default=True, + ) + use_file_save_reload: BoolProperty( + name="Save & Reload File", + description="File menu > Save & Reload, or Ctrl + Shift + W", + default=True, + ) + use_scene_refresh: BoolProperty( + name="Refresh Scene", + description="Specials Menu [W]", + default=True, + ) + use_timeline_extra_info: BoolProperty( + name="Timeline Extra Info", + description="Timeline Header", + default=True, + ) + use_image_node_display: BoolProperty( + name="Active Image Node in Editor", + description="Display active node image in image editor", + default=True, + ) + use_scene_stats: BoolProperty( + name="Extra Scene Statistics", + description="Display extra scene statistics in the status bar (may be slow in heavy scenes)", + default=False, + ) + frames_jump: IntProperty( + name="Frames", + description="Number of frames to jump forward/backward", + default=10, + min=1 + ) + use_framerate: BoolProperty( + name="Framerate Jump", + description="Jump the amount of frames forward/backward that you have set as your framerate", + default=False, + ) + use_layers_for_render: BoolProperty( + name="Current Layers for Render", + description="Save the layers that should be enabled for render", + default=True, + ) + + def draw(self, context): + layout = self.layout + + layout.label( + text="Here you can enable or disable specific tools, " + "in case they interfere with others or are just plain annoying") + + split = layout.split(factor=0.25) + + col = split.column() + sub = col.column(align=True) + sub.label(text="3D View", icon="VIEW3D") + sub.prop(self, "use_frame_current") + sub.prop(self, "use_scene_refresh") + + sub.separator() + + sub.label(text="General", icon="SCENE_DATA") + sub.prop(self, "use_file_save_reload") + sub.prop(self, "use_timeline_extra_info") + sub.prop(self, "use_scene_stats") + sub.prop(self, "use_layers_for_render") + sub.prop(self, "use_framerate") + + sub.separator() + + sub.label(text="Nodes Editor", icon="NODETREE") + sub.prop(self, "use_image_node_display") + + col = split.column() + sub = col.column(align=True) + sub.label(text="") + sub.label( + text="Set the current frame from the Specials menu in the 3D View [W]") + sub.label( + text="Refresh the current Scene. Hotkey: F5 or in Specials menu [W]") + + sub.separator() + sub.label(text="") # General icon + sub.label( + text="Quickly save and reload the current file (no warning!). " + "File menu or Ctrl+Shift+W") + sub.label( + text="SMPTE Timecode and frames left/ahead on Timeline's header") + sub.label( + text="Display extra stats for Scenes, Cameras, Meshlights (Cycles). Can be slow in heavy scenes") + sub.label( + text="Save the set of layers that should be activated for a final render") + sub.label( + text="Jump the amount of frames forward/backward that you've set as your framerate") + + sub.separator() + sub.label(text="") # Nodes + sub.label( + text="When double-clicking an Image node, display it on the Image editor " + "(if any)") + + +def register(): + bpy.utils.register_class(AmaranthToolsetPreferences) + + +def unregister(): + bpy.utils.unregister_class(AmaranthToolsetPreferences) diff --git a/amaranth/render/__init__.py b/amaranth/render/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/amaranth/render/__init__.py diff --git a/amaranth/render/border_camera.py b/amaranth/render/border_camera.py new file mode 100644 index 00000000..3d286080 --- /dev/null +++ b/amaranth/render/border_camera.py @@ -0,0 +1,64 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Set Camera Bounds as Render Border + +When in camera view, we can now set the border-render to be the same size +of the camera, so we don't render outside the view. Makes faster render +preview. Under Specials menu W, when in Camera view. +""" + +import bpy + + +class AMTH_VIEW3D_OT_render_border_camera(bpy.types.Operator): + + """Set camera bounds as render border""" + bl_idname = "view3d.render_border_camera" + bl_label = "Camera as Render Border" + + @classmethod + def poll(cls, context): + return context.space_data.region_3d.view_perspective == "CAMERA" + + def execute(self, context): + render = context.scene.render + render.use_border = True + render.border_min_x = 0 + render.border_min_y = 0 + render.border_max_x = 1 + render.border_max_y = 1 + + return {"FINISHED"} + + +def button_render_border_camera(self, context): + view3d = context.space_data.region_3d + + if view3d.view_perspective == "CAMERA": + layout = self.layout + layout.separator() + layout.operator(AMTH_VIEW3D_OT_render_border_camera.bl_idname, + text="Camera as Render Border", + icon="FULLSCREEN_ENTER") + + +def register(): + bpy.utils.register_class(AMTH_VIEW3D_OT_render_border_camera) + bpy.types.VIEW3D_MT_object_context_menu.append(button_render_border_camera) + + +def unregister(): + bpy.utils.unregister_class(AMTH_VIEW3D_OT_render_border_camera) + bpy.types.VIEW3D_MT_object_context_menu.remove(button_render_border_camera) diff --git a/amaranth/render/final_resolution.py b/amaranth/render/final_resolution.py new file mode 100644 index 00000000..deb81a66 --- /dev/null +++ b/amaranth/render/final_resolution.py @@ -0,0 +1,53 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +UI: Final Resolution + +Always wondered how big the render was going to be when rendering at a +certain %? +This feature displays a "Final Resolution" label with the size in pixels +of your render, it also displays the size for border renders. + +On the 'Dimensions' panel, Render properties. +""" +import bpy + + +def render_final_resolution_ui(self, context): + + rd = context.scene.render + layout = self.layout + + final_res_x = (rd.resolution_x * rd.resolution_percentage) / 100 + final_res_y = (rd.resolution_y * rd.resolution_percentage) / 100 + + if rd.use_border: + final_res_x_border = round( + (final_res_x * (rd.border_max_x - rd.border_min_x))) + final_res_y_border = round( + (final_res_y * (rd.border_max_y - rd.border_min_y))) + layout.label(text="Final Resolution: {} x {} [Border: {} x {}]".format( + str(final_res_x)[:-2], str(final_res_y)[:-2], + str(final_res_x_border), str(final_res_y_border))) + else: + layout.label(text="Final Resolution: {} x {}".format( + str(final_res_x)[:-2], str(final_res_y)[:-2])) + + +def register(): + bpy.types.RENDER_PT_dimensions.append(render_final_resolution_ui) + + +def unregister(): + bpy.types.RENDER_PT_dimensions.remove(render_final_resolution_ui) diff --git a/amaranth/render/meshlight_add.py b/amaranth/render/meshlight_add.py new file mode 100644 index 00000000..a4c4230c --- /dev/null +++ b/amaranth/render/meshlight_add.py @@ -0,0 +1,194 @@ +import bpy +from mathutils import Vector +from amaranth.utils import cycles_exists + + +# FEATURE: Add Meshlight +class AMTH_OBJECT_OT_meshlight_add(bpy.types.Operator): + + """Add a light emitting mesh""" + bl_idname = "object.meshlight_add" + bl_label = "Add Meshlight" + bl_options = {'REGISTER', 'UNDO'} + + single_sided: bpy.props.BoolProperty( + name="Single Sided", + default=True, + description="Only emit light on one side", + ) + + is_constant: bpy.props.BoolProperty( + name="Constant Falloff", + default=False, + description="Energy is constant (i.e. the Sun), " + "independent of how close to the source you are", + ) + + visible: bpy.props.BoolProperty( + name="Visible on Camera", + default=False, + description="Whether to show the meshlight source on Camera", + ) + + size: bpy.props.FloatProperty( + name="Size", + description="Meshlight size. Lower is sharper shadows, higher is softer", + min=0.01, max=100.0, + default=1.0, + ) + + strength: bpy.props.FloatProperty( + name="Strength", + min=0.01, max=100000.0, + default=1.5, + step=0.25, + ) + + temperature: bpy.props.FloatProperty( + name="Temperature", + min=800, max=12000.0, + default=5500.0, + step=800.0, + description="Temperature in Kelvin. Lower is warmer, higher is colder", + ) + + rotation: bpy.props.FloatVectorProperty( + name="Rotation", + subtype='EULER', + ) + + def execute(self, context): + scene = context.scene + # exists = False + number = 1 + + for obs in bpy.data.objects: + if obs.name.startswith("light_meshlight"): + number += 1 + + meshlight_name = 'light_meshlight_%.2d' % number + + bpy.ops.mesh.primitive_grid_add( + x_subdivisions=4, y_subdivisions=4, + rotation=self.rotation, size=self.size) + + bpy.context.object.name = meshlight_name + meshlight = scene.objects[meshlight_name] + meshlight.show_wire = True + meshlight.show_all_edges = True + + material = bpy.data.materials.get(meshlight_name) + + if not material: + material = bpy.data.materials.new(meshlight_name) + + bpy.ops.object.material_slot_add() + meshlight.active_material = material + + material.use_nodes = True + material.diffuse_color = (1, 0.5, 0, 1) + nodes = material.node_tree.nodes + links = material.node_tree.links + + # clear default nodes to start nice fresh + for no in nodes: + nodes.remove(no) + + if self.single_sided: + geometry = nodes.new(type="ShaderNodeNewGeometry") + + transparency = nodes.new(type="ShaderNodeBsdfTransparent") + transparency.inputs[0].default_value = (1, 1, 1, 1) + transparency.location = geometry.location + transparency.location += Vector((0.0, -55.0)) + + emission = nodes.new(type="ShaderNodeEmission") + emission.inputs['Strength'].default_value = self.strength + emission.location = transparency.location + emission.location += Vector((0.0, -80.0)) + + blackbody = nodes.new(type="ShaderNodeBlackbody") + blackbody.inputs['Temperature'].default_value = self.temperature + blackbody.location = emission.location + blackbody.location += Vector((-180.0, 0.0)) + blackbody.label = 'Temperature' + + mix = nodes.new(type="ShaderNodeMixShader") + mix.location = geometry.location + mix.location += Vector((180.0, 0.0)) + mix.inputs[2].show_expanded = True + + output = nodes.new(type="ShaderNodeOutputMaterial") + output.inputs[1].hide = True + output.inputs[2].hide = True + output.location = mix.location + output.location += Vector((180.0, 0.0)) + + # Make links + links.new(geometry.outputs['Backfacing'], mix.inputs[0]) + links.new(transparency.outputs['BSDF'], mix.inputs[1]) + links.new(emission.outputs['Emission'], mix.inputs[2]) + links.new(blackbody.outputs['Color'], emission.inputs['Color']) + links.new(mix.outputs['Shader'], output.inputs['Surface']) + + for sockets in geometry.outputs: + sockets.hide = True + else: + emission = nodes.new(type="ShaderNodeEmission") + emission.inputs['Strength'].default_value = self.strength + + blackbody = nodes.new(type="ShaderNodeBlackbody") + blackbody.inputs['Temperature'].default_value = self.temperature + blackbody.location = emission.location + blackbody.location += Vector((-180.0, 0.0)) + blackbody.label = 'Temperature' + + output = nodes.new(type="ShaderNodeOutputMaterial") + output.inputs[1].hide = True + output.inputs[2].hide = True + output.location = emission.location + output.location += Vector((180.0, 0.0)) + + links.new(blackbody.outputs['Color'], emission.inputs['Color']) + links.new(emission.outputs['Emission'], output.inputs['Surface']) + + if self.is_constant: + falloff = nodes.new(type="ShaderNodeLightFalloff") + falloff.inputs['Strength'].default_value = self.strength + falloff.location = emission.location + falloff.location += Vector((-180.0, -80.0)) + + links.new(falloff.outputs['Constant'], emission.inputs['Strength']) + + for sockets in falloff.outputs: + sockets.hide = True + + # so it shows slider on properties editor + for sockets in emission.inputs: + sockets.show_expanded = True + + material.cycles.sample_as_light = True + meshlight.cycles_visibility.shadow = False + meshlight.cycles_visibility.camera = self.visible + + return {'FINISHED'} + + +def ui_menu_lamps_add(self, context): + if cycles_exists() and context.scene.render.engine == 'CYCLES': + self.layout.separator() + self.layout.operator( + AMTH_OBJECT_OT_meshlight_add.bl_idname, + icon="LIGHT_AREA", text="Meshlight") + +# //FEATURE: Add Meshlight: Single Sided + + +def register(): + bpy.utils.register_class(AMTH_OBJECT_OT_meshlight_add) + bpy.types.VIEW3D_MT_light_add.append(ui_menu_lamps_add) + + +def unregister(): + bpy.utils.unregister_class(AMTH_OBJECT_OT_meshlight_add) + bpy.types.VIEW3D_MT_light_add.remove(ui_menu_lamps_add) diff --git a/amaranth/render/meshlight_select.py b/amaranth/render/meshlight_select.py new file mode 100644 index 00000000..cad3a95a --- /dev/null +++ b/amaranth/render/meshlight_select.py @@ -0,0 +1,63 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Select Meshlights + +Select all the meshes that emit light. On the header of the 3D View, top +of the select menu. +""" + +import bpy +from amaranth import utils + + +class AMTH_OBJECT_OT_select_meshlights(bpy.types.Operator): + + """Select light emitting meshes""" + bl_idname = "object.select_meshlights" + bl_label = "Select Meshlights" + bl_options = {"UNDO"} + + @classmethod + def poll(cls, context): + return context.scene.render.engine == "CYCLES" + + def execute(self, context): + # Deselect everything first + bpy.ops.object.select_all(action="DESELECT") + + for ob in context.scene.objects: + if utils.cycles_is_emission(context, ob): + ob.select_set(True) + context.view_layer.objects.active = ob + + if not context.selected_objects and not context.view_layer.objects.active: + self.report({"INFO"}, "No meshlights to select") + + return {"FINISHED"} + + +def button_select_meshlights(self, context): + if utils.cycles_exists() and utils.cycles_active(context): + self.layout.operator('object.select_meshlights', icon="LIGHT_SUN") + + +def register(): + bpy.utils.register_class(AMTH_OBJECT_OT_select_meshlights) + bpy.types.VIEW3D_MT_select_object.append(button_select_meshlights) + + +def unregister(): + bpy.utils.unregister_class(AMTH_OBJECT_OT_select_meshlights) + bpy.types.VIEW3D_MT_select_object.remove(button_select_meshlights) diff --git a/amaranth/render/passepartout.py b/amaranth/render/passepartout.py new file mode 100644 index 00000000..7d65299c --- /dev/null +++ b/amaranth/render/passepartout.py @@ -0,0 +1,45 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Passepartout on Specials menu + +The passepartout value of local cameras is now available on the Specials +menu for easy access. +Under Specials menu W, when in Camera view. +""" + +import bpy + + +def button_camera_passepartout(self, context): + view3d = context.space_data.region_3d + cam = context.scene.camera + + if view3d.view_perspective == "CAMERA": + if cam is None or not hasattr(cam, "data") or cam.type != "CAMERA": + return + + layout = self.layout + if cam.data.show_passepartout: + layout.prop(cam.data, "passepartout_alpha", text="Passepartout") + else: + layout.prop(cam.data, "show_passepartout") + + +def register(): + bpy.types.VIEW3D_MT_object_context_menu.append(button_camera_passepartout) + + +def unregister(): + bpy.types.VIEW3D_MT_object_context_menu.remove(button_camera_passepartout) diff --git a/amaranth/render/render_output_z.py b/amaranth/render/render_output_z.py new file mode 100644 index 00000000..bbde91ff --- /dev/null +++ b/amaranth/render/render_output_z.py @@ -0,0 +1,53 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +EXR Render: Warn when Z not connected +Display a little warning label when exporting EXR, with Z Buffer enabled, but +forgot to plug the Z input in the Compositor. + +Might be a bit too specific, but found it nice to remember to plug the Z input +if we explicitely specify for Z Buffers to be saved (because it's disabled by +default). + +Find it on the Output panel, Render properties. +""" +import bpy + + +# // FEATURE: Object ID for objects inside DupliGroups +# UI: Warning about Z not connected when using EXR +def ui_render_output_z(self, context): + + scene = bpy.context.scene + image = scene.render.image_settings + if scene.render.use_compositing and \ + image.file_format == 'OPEN_EXR' and \ + image.use_zbuffer: + if scene.node_tree and scene.node_tree.nodes: + for no in scene.node_tree.nodes: + if no.type == 'COMPOSITE': + if not no.inputs['Z'].is_linked: + self.layout.label( + text="The Z output in node \"%s\" is not connected" % + no.name, icon="ERROR") + +# // UI: Warning about Z not connected + + +def register(): + bpy.types.RENDER_PT_output.append(ui_render_output_z) + + +def unregister(): + bpy.types.RENDER_PT_output.remove(ui_render_output_z) diff --git a/amaranth/render/samples_scene.py b/amaranth/render/samples_scene.py new file mode 100644 index 00000000..87e94c4c --- /dev/null +++ b/amaranth/render/samples_scene.py @@ -0,0 +1,254 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Cycles: Samples per Scene + +When working in production, it's often more convenient to do lighting and +compositing in different scenes (so you can later append the comp scene + to bring together nodes, settings, lamps, RenderLayers). + +This would lead to work with more than one scene. When doing render tests +you want to know at a glance how many samples the other scenes have, +without manually switching. This is the idea behind the feature. + +Find it on the Sampling panel, on Render properties. +Developed during Caminandes Open Movie Project +""" + +import bpy +from amaranth import utils +from bpy.props import ( + BoolProperty, + IntProperty, + ) + + +class AMTH_RENDER_OT_cycles_samples_percentage_set(bpy.types.Operator): + + """Save the current number of samples per shader as final (gets saved in .blend)""" + bl_idname = "scene.amaranth_cycles_samples_percentage_set" + bl_label = "Set as Render Samples" + + def execute(self, context): + cycles = context.scene.cycles + cycles.use_samples_final = True + + context.scene["amth_cycles_samples_final"] = [ + cycles.diffuse_samples, + cycles.glossy_samples, + cycles.transmission_samples, + cycles.ao_samples, + cycles.mesh_light_samples, + cycles.subsurface_samples, + cycles.volume_samples] + + self.report({"INFO"}, "Render Samples Saved") + + return {"FINISHED"} + + +class AMTH_RENDER_OT_cycles_samples_percentage(bpy.types.Operator): + + """Set a percentage of the final render samples""" + bl_idname = "scene.amaranth_cycles_samples_percentage" + bl_label = "Set Render Samples Percentage" + + percent: IntProperty( + name="Percentage", + description="Percentage to divide render samples by", + subtype="PERCENTAGE", default=0 + ) + + def execute(self, context): + percent = self.percent + cycles = context.scene.cycles + cycles_samples_final = context.scene["amth_cycles_samples_final"] + + cycles.use_samples_final = False + + if percent == 100: + cycles.use_samples_final = True + + cycles.diffuse_samples = int((cycles_samples_final[0] / 100) * percent) + cycles.glossy_samples = int((cycles_samples_final[1] / 100) * percent) + cycles.transmission_samples = int( + (cycles_samples_final[2] / 100) * percent) + cycles.ao_samples = int((cycles_samples_final[3] / 100) * percent) + cycles.mesh_light_samples = int( + (cycles_samples_final[4] / 100) * percent) + cycles.subsurface_samples = int( + (cycles_samples_final[5] / 100) * percent) + cycles.volume_samples = int((cycles_samples_final[6] / 100) * percent) + + return {"FINISHED"} + + +def render_cycles_scene_samples(self, context): + + layout = self.layout + scene = context.scene + render = scene.render + if utils.cycles_exists(): + cscene = scene.cycles + list_sampling = scene.amaranth_cycles_list_sampling + + # Set Render Samples + if utils.cycles_exists() and cscene.progressive == "BRANCHED_PATH": + layout.separator() + split = layout.split() + col = split.column() + + col.operator( + AMTH_RENDER_OT_cycles_samples_percentage_set.bl_idname, + text="%s" % + "Set as Render Samples" if cscene.use_samples_final else "Set New Render Samples", + icon="%s" % + "PINNED" if cscene.use_samples_final else "UNPINNED") + + col = split.column() + row = col.row(align=True) + row.enabled = True if scene.get("amth_cycles_samples_final") else False + + row.operator( + AMTH_RENDER_OT_cycles_samples_percentage.bl_idname, + text="100%").percent = 100 + row.operator( + AMTH_RENDER_OT_cycles_samples_percentage.bl_idname, + text="75%").percent = 75 + row.operator( + AMTH_RENDER_OT_cycles_samples_percentage.bl_idname, + text="50%").percent = 50 + row.operator( + AMTH_RENDER_OT_cycles_samples_percentage.bl_idname, + text="25%").percent = 25 + + # List Samples + #if (len(scene.render.layers) > 1) or (len(bpy.data.scenes) > 1): + if (len(scene.render.views) > 1) or (len(bpy.data.scenes) > 1): + + box = layout.box() + row = box.row(align=True) + col = row.column(align=True) + + row = col.row(align=True) + row.alignment = "LEFT" + row.prop(scene, "amaranth_cycles_list_sampling", + icon="%s" % "TRIA_DOWN" if list_sampling else "TRIA_RIGHT", + emboss=False) + + if list_sampling: + #if len(scene.render.layers) == 1 and render.layers[0].samples == 0: + if len(scene.render.views) == 1 and render.view_layers[0].samples == 0: + pass + else: + col.separator() + #col.label(text="RenderLayers:", icon="RENDERLAYERS") + col.label(text="View Layers:", icon="RENDERLAYERS") + + #for rl in scene.render.layers: + for rl in scene.view_layers: + row = col.row(align=True) + row.label(text=rl.name, icon="BLANK1") + row.prop( + rl, "samples", text="%s" % + "Samples" if rl.samples > 0 else "Automatic (%s)" % + (cscene.aa_samples if cscene.progressive == "BRANCHED_PATH" else cscene.samples)) + + if (len(bpy.data.scenes) > 1): + col.separator() + + col.label(text="Scenes:", icon="SCENE_DATA") + + if utils.cycles_exists() and cscene.progressive == "PATH": + for s in bpy.data.scenes: + if s != scene: + row = col.row(align=True) + if s.render.engine == "CYCLES": + cscene = s.cycles + + #row.label(s.name) + row.label(text=s.name) + row.prop(cscene, "samples", icon="BLANK1") + else: + row.label( + text="Scene: '%s' is not using Cycles" % + s.name) + else: + for s in bpy.data.scenes: + if s != scene: + row = col.row(align=True) + if s.render.engine == "CYCLES": + cscene = s.cycles + + row.label(text=s.name, icon="BLANK1") + row.prop(cscene, "aa_samples", + text="AA Samples") + else: + row.label( + text="Scene: '%s' is not using Cycles" % + s.name) + + +def init(): + scene = bpy.types.Scene + if utils.cycles_exists(): + scene.amaranth_cycles_list_sampling = bpy.props.BoolProperty( + default=False, + name="Samples Per:") + # Note: add versioning code to adress changes introduced in 2.79.1 + if bpy.app.version >= (2, 79, 1): + from cycles import properties as _cycles_props + _cycles_props.CyclesRenderSettings.use_samples_final = BoolProperty( + name="Use Final Render Samples", + description="Use current shader samples as final render samples", + default=False + ) + else: + bpy.types.CyclesRenderSettings.use_samples_final = BoolProperty( + name="Use Final Render Samples", + description="Use current shader samples as final render samples", + default=False + ) + + + +def clear(): + wm = bpy.context.window_manager + for p in ("amarath_cycles_list_sampling", "use_samples_final"): + if p in wm: + del wm[p] + + +def register(): + init() + bpy.utils.register_class(AMTH_RENDER_OT_cycles_samples_percentage) + bpy.utils.register_class(AMTH_RENDER_OT_cycles_samples_percentage_set) + if utils.cycles_exists(): + if bpy.app.version >= (2, 79, 1): + bpy.types.CYCLES_RENDER_PT_sampling.append(render_cycles_scene_samples) + else: + bpy.types.CyclesRender_PT_sampling.append(render_cycles_scene_samples) + + +def unregister(): + bpy.utils.unregister_class(AMTH_RENDER_OT_cycles_samples_percentage) + bpy.utils.unregister_class(AMTH_RENDER_OT_cycles_samples_percentage_set) + if utils.cycles_exists(): + if bpy.app.version >= (2, 79, 1): + bpy.types.CYCLES_RENDER_PT_sampling.remove(render_cycles_scene_samples) + else: + bpy.types.CyclesRender_PT_sampling.remove(render_cycles_scene_samples) + + + clear() diff --git a/amaranth/scene/__init__.py b/amaranth/scene/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/amaranth/scene/__init__.py diff --git a/amaranth/scene/current_blend.py b/amaranth/scene/current_blend.py new file mode 100644 index 00000000..e3f4ca91 --- /dev/null +++ b/amaranth/scene/current_blend.py @@ -0,0 +1,80 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +File Browser > Go to Current Blend's Folder + +For when you're lost browsing files and want to go back to the currently +open blend's directory. Look for it on the File Browser's header, only +shows up if the file is saved. +""" + +import bpy + +# From space_filebrowser.py +def panel_poll_is_upper_region(region): + # The upper region is left-aligned, the lower is split into it then. + # Note that after "Flip Regions" it's right-aligned. + return region.alignment in {'LEFT', 'RIGHT'} + + +class AMTH_FILE_OT_directory_current_blend(bpy.types.Operator): + + """Go to the directory of the currently open blend file""" + bl_idname = "file.directory_current_blend" + bl_label = "Current Blend's Folder" + + def execute(self, context): + bpy.ops.file.select_bookmark(dir="//") + return {"FINISHED"} + + +class FILEBROWSER_PT_amaranth(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOLS' + bl_category = "Bookmarks" + bl_label = "Amaranth" + bl_options = {'HIDE_HEADER'} + + @classmethod + def poll(cls, context): + return panel_poll_is_upper_region(context.region) + + def draw(self, context): + layout = self.layout + layout.scale_x = 1.3 + layout.scale_y = 1.3 + + if bpy.data.filepath: + row = layout.row() + flow = row.grid_flow(row_major=False, columns=0, even_columns=False, even_rows=False, align=True) + + subrow = flow.row() + subsubrow = subrow.row(align=True) + subsubrow.operator( + AMTH_FILE_OT_directory_current_blend.bl_idname, + icon="DESKTOP") + + +classes = ( + AMTH_FILE_OT_directory_current_blend, + FILEBROWSER_PT_amaranth +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/amaranth/scene/debug.py b/amaranth/scene/debug.py new file mode 100755 index 00000000..c4962b5d --- /dev/null +++ b/amaranth/scene/debug.py @@ -0,0 +1,1408 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Scene Debug Panel + +This is something I've been wanting to have for a while, a way to know +certain info about your scene. A way to "debug" it, especially when +working in production with other teams, this came in very handy. + +Being mostly a lighting guy myself, I needed two main features to start with: + +* List Cycles Material using X shader +Where X is any shader type you want. It will display (and print on console) +a list of all the materials containing the shader you specified above. +Good for finding out if there's any Meshlight (Emission) material hidden, +or if there are many glossy shaders making things noisy. +A current limitation is that it doesn't look inside node groups (yet, +working on it!). It works since 0.8.8! + +Under the "Scene Debug" panel in Scene properties. + +* Lighter's Corner +This is an UI List of Lights in the scene(s). +It allows you to quickly see how many lights you have, select them by +clicking on their name, see their type (icon), samples number (if using +Branched Path Tracing), size, and change their visibility. + +""" + +# TODO: module cleanup! maybe break it up in a package +# dicts instead of if, elif, else all over the place. +# helper functions instead of everything on the execute method. +# str.format() + dicts instead of inline % op all over the place. +# remove/manage debug print calls. +# avoid duplicate code/patterns through helper functions. + +import os +import bpy +from amaranth import utils +from bpy.types import ( + Operator, + Panel, + UIList, + PropertyGroup, + ) +from bpy.props import ( + BoolProperty, + CollectionProperty, + EnumProperty, + IntProperty, + PointerProperty, + StringProperty, + ) + +# default string used in the List Users for Datablock section menus +USER_X_NAME_EMPTY = "Data Block not selected/existing" + + +class AMTH_store_data(): + # used by: AMTH_SCENE_OT_list_users_for_x operator + users = { + 'OBJECT_DATA': [], # Store Objects with Material + 'MATERIAL': [], # Materials (Node tree) + 'LIGHT': [], # Lights + 'WORLD': [], # World + 'TEXTURE': [], # Textures (Psys, Brushes) + 'MODIFIER': [], # Modifiers + 'MESH_DATA': [], # Vertex Colors + 'VIEW3D': [], # Background Images + 'NODETREE': [], # Compositor + } + libraries = [] # Libraries x type + + # used by: AMTH_SCENE_OT_list_missing_material_slots operator + obj_mat_slots = [] # Missing material slots + obj_mat_slots_lib = [] # Libraries with missing material slots + + # used by: AMTH_SCENE_OT_cycles_shader_list_nodes operator + mat_shaders = [] # Materials that use a specific shader + + # used by : AMTH_SCENE_OT_list_missing_node_links operator + count_groups = 0 # Missing node groups count + count_images = 0 # Missing node images + count_image_node_unlinked = 0 # Unlinked Image nodes + + +def call_update_datablock_type(self, context): + try: + # Note: this is pretty weak, but updates the operator enum selection + bpy.ops.scene.amth_list_users_for_x_type(list_type_select='0') + except: + pass + + +def init(): + scene = bpy.types.Scene + + scene.amaranth_lighterscorner_list_meshlights = BoolProperty( + default=False, + name="List Meshlights", + description="Include light emitting meshes on the list" + ) + amth_datablock_types = ( + ("IMAGE_DATA", "Image", "Image Datablocks", 0), + ("MATERIAL", "Material", "Material Datablocks", 1), + ("GROUP_VCOL", "Vertex Colors", "Vertex Color Layers", 2), + ) + scene.amth_datablock_types = EnumProperty( + items=amth_datablock_types, + name="Type", + description="Datablock Type", + default="MATERIAL", + update=call_update_datablock_type, + options={"SKIP_SAVE"} + ) + if utils.cycles_exists(): + cycles_shader_node_types = ( + ("BSDF_DIFFUSE", "Diffuse BSDF", "", 0), + ("BSDF_GLOSSY", "Glossy BSDF", "", 1), + ("BSDF_TRANSPARENT", "Transparent BSDF", "", 2), + ("BSDF_REFRACTION", "Refraction BSDF", "", 3), + ("BSDF_GLASS", "Glass BSDF", "", 4), + ("BSDF_TRANSLUCENT", "Translucent BSDF", "", 5), + ("BSDF_ANISOTROPIC", "Anisotropic BSDF", "", 6), + ("BSDF_VELVET", "Velvet BSDF", "", 7), + ("BSDF_TOON", "Toon BSDF", "", 8), + ("SUBSURFACE_SCATTERING", "Subsurface Scattering", "", 9), + ("EMISSION", "Emission", "", 10), + ("BSDF_HAIR", "Hair BSDF", "", 11), + ("BACKGROUND", "Background", "", 12), + ("AMBIENT_OCCLUSION", "Ambient Occlusion", "", 13), + ("HOLDOUT", "Holdout", "", 14), + ("VOLUME_ABSORPTION", "Volume Absorption", "", 15), + ("VOLUME_SCATTER", "Volume Scatter", "", 16), + ("MIX_SHADER", "Mix Shader", "", 17), + ("ADD_SHADER", "Add Shader", "", 18), + ('BSDF_PRINCIPLED', 'Principled BSDF', "", 19), + ) + scene.amaranth_cycles_node_types = EnumProperty( + items=cycles_shader_node_types, + name="Shader" + ) + + +def clear(): + props = ( + "amaranth_cycles_node_types", + "amaranth_lighterscorner_list_meshlights", + ) + wm = bpy.context.window_manager + for p in props: + if wm.get(p): + del wm[p] + + +def print_with_count_list(text="", send_list=[]): + if text: + print("\n* {}\n".format(text)) + if not send_list: + print("List is empty, no items to display") + return + + for i, entry in enumerate(send_list): + print('{:02d}. {}'.format(i + 1, send_list[i])) + print("\n") + + +def print_grammar(line="", single="", multi="", cond=[]): + phrase = single if len(cond) == 1 else multi + print("\n* {} {}:\n".format(line, phrase)) + + +def reset_global_storage(what="NONE"): + if what == "NONE": + return + + if what == "XTYPE": + for user in AMTH_store_data.users: + AMTH_store_data.users[user] = [] + AMTH_store_data.libraries = [] + + elif what == "MAT_SLOTS": + AMTH_store_data.obj_mat_slots[:] = [] + AMTH_store_data.obj_mat_slots_lib[:] = [] + + elif what == "NODE_LINK": + AMTH_store_data.obj_mat_slots[:] = [] + AMTH_store_data.count_groups = 0 + AMTH_store_data.count_images = 0 + AMTH_store_data.count_image_node_unlinked = 0 + + elif what == "SHADER": + AMTH_store_data.mat_shaders[:] = [] + + +class AMTH_SCENE_OT_cycles_shader_list_nodes(Operator): + """List Cycles materials containing a specific shader""" + bl_idname = "scene.cycles_list_nodes" + bl_label = "List Materials" + + @classmethod + def poll(cls, context): + return utils.cycles_exists() and utils.cycles_active(context) + + def execute(self, context): + node_type = context.scene.amaranth_cycles_node_types + roughness = False + shaders_roughness = ("BSDF_GLOSSY", "BSDF_DIFFUSE", "BSDF_GLASS") + + reset_global_storage("SHADER") + + print("\n=== Cycles Shader Type: {} === \n".format(node_type)) + + for ma in bpy.data.materials: + if not ma.node_tree: + continue + + nodes = ma.node_tree.nodes + print_unconnected = ( + "Note: \nOutput from \"{}\" node in material \"{}\" " + "not connected\n".format(node_type, ma.name) + ) + + for no in nodes: + if no.type == node_type: + for ou in no.outputs: + if ou.links: + connected = True + if no.type in shaders_roughness: + roughness = "R: {:.4f}".format( + no.inputs["Roughness"].default_value + ) + else: + roughness = False + else: + connected = False + print(print_unconnected) + + if ma.name not in AMTH_store_data.mat_shaders: + AMTH_store_data.mat_shaders.append( + "%s%s [%s] %s%s%s" % + ("[L] " if ma.library else "", + ma.name, + ma.users, + "[F]" if ma.use_fake_user else "", + " - [%s]" % + roughness if roughness else "", + " * Output not connected" if not connected else "") + ) + elif no.type == "GROUP": + if no.node_tree: + for nog in no.node_tree.nodes: + if nog.type == node_type: + for ou in nog.outputs: + if ou.links: + connected = True + if nog.type in shaders_roughness: + roughness = "R: {:.4f}".format( + nog.inputs["Roughness"].default_value + ) + else: + roughness = False + else: + connected = False + print(print_unconnected) + + if ma.name not in AMTH_store_data.mat_shaders: + AMTH_store_data.mat_shaders.append( + '%s%s%s [%s] %s%s%s' % + ("[L] " if ma.library else "", + "Node Group: %s%s -> " % + ("[L] " if no.node_tree.library else "", + no.node_tree.name), + ma.name, + ma.users, + "[F]" if ma.use_fake_user else "", + " - [%s]" % + roughness if roughness else "", + " * Output not connected" if not connected else "") + ) + AMTH_store_data.mat_shaders = sorted(list(set(AMTH_store_data.mat_shaders))) + + message = "No materials with nodes type {} found".format(node_type) + if len(AMTH_store_data.mat_shaders) > 0: + message = "A total of {} {} using {} found".format( + len(AMTH_store_data.mat_shaders), + "material" if len(AMTH_store_data.mat_shaders) == 1 else "materials", + node_type) + print_with_count_list(send_list=AMTH_store_data.mat_shaders) + + self.report({'INFO'}, message) + AMTH_store_data.mat_shaders = sorted(list(set(AMTH_store_data.mat_shaders))) + + return {"FINISHED"} + + +class AMTH_SCENE_OT_amaranth_object_select(Operator): + """Select object""" + bl_idname = "scene.amaranth_object_select" + bl_label = "Select Object" + + object_name: StringProperty() + + def execute(self, context): + if not (self.object_name and self.object_name in bpy.data.objects): + self.report({'WARNING'}, + "Object with the given name could not be found. Operation Cancelled") + return {"CANCELLED"} + + obj = bpy.data.objects[self.object_name] + + bpy.ops.object.select_all(action="DESELECT") + obj.select_set(True) + context.view_layer.objects.active = obj + + return {"FINISHED"} + + +class AMTH_SCENE_OT_list_missing_node_links(Operator): + """Print a list of missing node links""" + bl_idname = "scene.list_missing_node_links" + bl_label = "List Missing Node Links" + + def execute(self, context): + missing_groups = [] + missing_images = [] + image_nodes_unlinked = [] + libraries = [] + + reset_global_storage(what="NODE_LINK") + + for ma in bpy.data.materials: + if not ma.node_tree: + continue + + for no in ma.node_tree.nodes: + if no.type == "GROUP": + if not no.node_tree: + AMTH_store_data.count_groups += 1 + + users_ngroup = [] + + for ob in bpy.data.objects: + if ob.material_slots and ma.name in ob.material_slots: + users_ngroup.append("%s%s%s" % ( + "[L] " if ob.library else "", + "[F] " if ob.use_fake_user else "", + ob.name)) + + missing_groups.append( + "MA: %s%s%s [%s]%s%s%s\n" % + ("[L] " if ma.library else "", + "[F] " if ma.use_fake_user else "", + ma.name, + ma.users, + " *** No users *** " if ma.users == 0 else "", + "\nLI: %s" % + ma.library.filepath if ma.library else "", + "\nOB: %s" % + ", ".join(users_ngroup) if users_ngroup else "") + ) + if ma.library: + libraries.append(ma.library.filepath) + + if no.type == "TEX_IMAGE": + + outputs_empty = not no.outputs["Color"].is_linked and \ + not no.outputs["Alpha"].is_linked + + if no.image: + image_path_exists = os.path.exists( + bpy.path.abspath( + no.image.filepath, + library=no.image.library) + ) + + if outputs_empty or not no.image or not image_path_exists: + + users_images = [] + + for ob in bpy.data.objects: + if ob.material_slots and ma.name in ob.material_slots: + users_images.append("%s%s%s" % ( + "[L] " if ob.library else "", + "[F] " if ob.use_fake_user else "", + ob.name)) + + if outputs_empty: + AMTH_store_data.count_image_node_unlinked += 1 + + image_nodes_unlinked.append( + "%s%s%s%s%s [%s]%s%s%s%s%s\n" % + ("NO: %s" % + no.name, + "\nMA: ", + "[L] " if ma.library else "", + "[F] " if ma.use_fake_user else "", + ma.name, + ma.users, + " *** No users *** " if ma.users == 0 else "", + "\nLI: %s" % + ma.library.filepath if ma.library else "", + "\nIM: %s" % + no.image.name if no.image else "", + "\nLI: %s" % + no.image.filepath if no.image and no.image.filepath else "", + "\nOB: %s" % + ', '.join(users_images) if users_images else "")) + + if not no.image or not image_path_exists: + AMTH_store_data.count_images += 1 + + missing_images.append( + "MA: %s%s%s [%s]%s%s%s%s%s\n" % + ("[L] " if ma.library else "", + "[F] " if ma.use_fake_user else "", + ma.name, + ma.users, + " *** No users *** " if ma.users == 0 else "", + "\nLI: %s" % + ma.library.filepath if ma.library else "", + "\nIM: %s" % + no.image.name if no.image else "", + "\nLI: %s" % + no.image.filepath if no.image and no.image.filepath else "", + "\nOB: %s" % + ', '.join(users_images) if users_images else "")) + + if ma.library: + libraries.append(ma.library.filepath) + + # Remove duplicates and sort + missing_groups = sorted(list(set(missing_groups))) + missing_images = sorted(list(set(missing_images))) + image_nodes_unlinked = sorted(list(set(image_nodes_unlinked))) + libraries = sorted(list(set(libraries))) + + print( + "\n\n== %s missing image %s, %s missing node %s and %s image %s unlinked ==" % + ("No" if AMTH_store_data.count_images == 0 else str( + AMTH_store_data.count_images), + "node" if AMTH_store_data.count_images == 1 else "nodes", + "no" if AMTH_store_data.count_groups == 0 else str( + AMTH_store_data.count_groups), + "group" if AMTH_store_data.count_groups == 1 else "groups", + "no" if AMTH_store_data.count_image_node_unlinked == 0 else str( + AMTH_store_data.count_image_node_unlinked), + "node" if AMTH_store_data.count_groups == 1 else "nodes") + ) + # List Missing Node Groups + if missing_groups: + print_with_count_list("Missing Node Group Links", missing_groups) + + # List Missing Image Nodes + if missing_images: + print_with_count_list("Missing Image Nodes Link", missing_images) + + # List Image Nodes with its outputs unlinked + if image_nodes_unlinked: + print_with_count_list("Image Nodes Unlinked", image_nodes_unlinked) + + if missing_groups or missing_images or image_nodes_unlinked: + if libraries: + print_grammar("That's bad, run check", "this library", "these libraries", libraries) + print_with_count_list(send_list=libraries) + else: + self.report({"INFO"}, "Yay! No missing node links") + + if missing_groups and missing_images: + self.report( + {"WARNING"}, + "%d missing image %s and %d missing node %s found" % + (AMTH_store_data.count_images, + "node" if AMTH_store_data.count_images == 1 else "nodes", + AMTH_store_data.count_groups, + "group" if AMTH_store_data.count_groups == 1 else "groups") + ) + + return {"FINISHED"} + + +class AMTH_SCENE_OT_list_missing_material_slots(Operator): + """List objects with empty material slots""" + bl_idname = "scene.list_missing_material_slots" + bl_label = "List Empty Material Slots" + + def execute(self, context): + reset_global_storage("MAT_SLOTS") + + for ob in bpy.data.objects: + for ma in ob.material_slots: + if not ma.material: + AMTH_store_data.obj_mat_slots.append('{}{}'.format( + '[L] ' if ob.library else '', ob.name)) + if ob.library: + AMTH_store_data.obj_mat_slots_lib.append(ob.library.filepath) + + AMTH_store_data.obj_mat_slots = sorted(list(set(AMTH_store_data.obj_mat_slots))) + AMTH_store_data.obj_mat_slots_lib = sorted(list(set(AMTH_store_data.obj_mat_slots_lib))) + + if len(AMTH_store_data.obj_mat_slots) == 0: + self.report({"INFO"}, + "No objects with empty material slots found") + return {"FINISHED"} + + print( + "\n* A total of {} {} with empty material slots was found \n".format( + len(AMTH_store_data.obj_mat_slots), + "object" if len(AMTH_store_data.obj_mat_slots) == 1 else "objects") + ) + print_with_count_list(send_list=AMTH_store_data.obj_mat_slots) + + if AMTH_store_data.obj_mat_slots_lib: + print_grammar("Check", "this library", "these libraries", + AMTH_store_data.obj_mat_slots_lib + ) + print_with_count_list(send_list=AMTH_store_data.obj_mat_slots_lib) + + return {"FINISHED"} + + +class AMTH_SCENE_OT_list_users_for_x_type(Operator): + bl_idname = "scene.amth_list_users_for_x_type" + bl_label = "Select" + bl_description = "Select Datablock Name" + + @staticmethod + def fill_where(): + where = [] + data_block = bpy.context.scene.amth_datablock_types + + if data_block == 'IMAGE_DATA': + for im in bpy.data.images: + if im.name not in {'Render Result', 'Viewer Node'}: + where.append(im) + + elif data_block == 'MATERIAL': + where = bpy.data.materials + + elif data_block == 'GROUP_VCOL': + for ob in bpy.data.objects: + if ob.type == 'MESH': + for v in ob.data.vertex_colors: + if v and v not in where: + where.append(v) + where = list(set(where)) + + return where + + def avail(self, context): + datablock_type = bpy.context.scene.amth_datablock_types + where = AMTH_SCENE_OT_list_users_for_x_type.fill_where() + items = [(str(i), x.name, x.name, datablock_type, i) for i, x in enumerate(where)] + items = sorted(list(set(items))) + if not items: + items = [('0', USER_X_NAME_EMPTY, USER_X_NAME_EMPTY, "INFO", 0)] + return items + + list_type_select: EnumProperty( + items=avail, + name="Available", + options={"SKIP_SAVE"} + ) + + @classmethod + def poll(cls, context): + return bpy.context.scene.amth_datablock_types + + def execute(self, context): + where = self.fill_where() + bpy.context.scene.amth_list_users_for_x_name = \ + where[int(self.list_type_select)].name if where else USER_X_NAME_EMPTY + + return {'FINISHED'} + + +class AMTH_SCENE_OT_list_users_for_x(Operator): + """List users for a particular datablock""" + bl_idname = "scene.amth_list_users_for_x" + bl_label = "List Users for Datablock" + + name: StringProperty() + + def execute(self, context): + d = bpy.data + x = self.name if self.name else context.scene.amth_list_users_for_x_name + + if USER_X_NAME_EMPTY in x: + self.report({'INFO'}, + "Please select a DataBlock name first. Operation Cancelled") + return {"CANCELLED"} + + dtype = context.scene.amth_datablock_types + + reset_global_storage("XTYPE") + + # IMAGE TYPE + if dtype == 'IMAGE_DATA': + # Check Materials + for ma in d.materials: + # Cycles + if utils.cycles_exists(): + if ma and ma.node_tree and ma.node_tree.nodes: + materials = [] + + for nd in ma.node_tree.nodes: + if nd and nd.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}: + materials.append(nd) + + if nd and nd.type == 'GROUP': + if nd.node_tree and nd.node_tree.nodes: + for ng in nd.node_tree.nodes: + if ng.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'}: + materials.append(ng) + + for no in materials: + if no.image and no.image.name == x: + objects = [] + + for ob in d.objects: + if ma.name in ob.material_slots: + objects.append(ob.name) + links = False + + for o in no.outputs: + if o.links: + links = True + + name = '"{0}" {1}{2}'.format( + ma.name, + 'in object: {0}'.format(objects) if objects else ' (unassigned)', + '' if links else ' (unconnected)') + + if name not in AMTH_store_data.users['MATERIAL']: + AMTH_store_data.users['MATERIAL'].append(name) + # Check Lights + for la in d.lights: + # Cycles + if utils.cycles_exists(): + if la and la.node_tree and la.node_tree.nodes: + for no in la.node_tree.nodes: + if no and \ + no.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \ + no.image and no.image.name == x: + if la.name not in AMTH_store_data.users['LIGHT']: + AMTH_store_data.users['LIGHT'].append(la.name) + # Check World + for wo in d.worlds: + # Cycles + if utils.cycles_exists(): + if wo and wo.node_tree and wo.node_tree.nodes: + for no in wo.node_tree.nodes: + if no and \ + no.type in {'TEX_IMAGE', 'TEX_ENVIRONMENT'} and \ + no.image and no.image.name == x: + if wo.name not in AMTH_store_data.users['WORLD']: + AMTH_store_data.users['WORLD'].append(wo.name) + # Check Textures + for te in d.textures: + if te and te.type == 'IMAGE' and te.image: + name = te.image.name + + if name == x and \ + name not in AMTH_store_data.users['TEXTURE']: + AMTH_store_data.users['TEXTURE'].append(te.name) + # Check Modifiers in Objects + for ob in d.objects: + for mo in ob.modifiers: + if mo.type in {'UV_PROJECT'}: + image = mo.image + + if mo and image and image.name == x: + name = '"{0}" modifier in {1}'.format(mo.name, ob.name) + if name not in AMTH_store_data.users['MODIFIER']: + AMTH_store_data.users['MODIFIER'].append(name) + # Check Background Images in Viewports + for scr in d.screens: + for ar in scr.areas: + if ar.type == 'VIEW_3D': + if ar.spaces and \ + ar.spaces.active and \ + ar.spaces.active.background_images: + for bg in ar.spaces.active.background_images: + image = bg.image + + if bg and image and image.name == x: + name = 'Background for 3D Viewport in Screen "{0}"'\ + .format(scr.name) + if name not in AMTH_store_data.users['VIEW3D']: + AMTH_store_data.users['VIEW3D'].append(name) + # Check the Compositor + for sce in d.scenes: + if sce.node_tree and sce.node_tree.nodes: + nodes = [] + for nd in sce.node_tree.nodes: + if nd.type == 'IMAGE': + nodes.append(nd) + elif nd.type == 'GROUP': + if nd.node_tree and nd.node_tree.nodes: + for ng in nd.node_tree.nodes: + if ng.type == 'IMAGE': + nodes.append(ng) + + for no in nodes: + if no.image and no.image.name == x: + links = False + + for o in no.outputs: + if o.links: + links = True + + name = 'Node {0} in Compositor (Scene "{1}"){2}'.format( + no.name, + sce.name, + '' if links else ' (unconnected)') + + if name not in AMTH_store_data.users['NODETREE']: + AMTH_store_data.users['NODETREE'].append(name) + # MATERIAL TYPE + if dtype == 'MATERIAL': + # Check Materials - Note: build an object_check list as only strings are stored + object_check = [d.objects[names] for names in AMTH_store_data.users['OBJECT_DATA'] if + names in d.objects] + for ob in d.objects: + for ma in ob.material_slots: + if ma.name == x: + if ma not in object_check: + AMTH_store_data.users['OBJECT_DATA'].append(ob.name) + + if ob.library: + AMTH_store_data.libraries.append(ob.library.filepath) + # VERTEX COLOR TYPE + elif dtype == 'GROUP_VCOL': + # Check VCOL in Meshes + for ob in bpy.data.objects: + if ob.type == 'MESH': + for v in ob.data.vertex_colors: + if v.name == x: + name = '{0}'.format(ob.name) + + if name not in AMTH_store_data.users['MESH_DATA']: + AMTH_store_data.users['MESH_DATA'].append(name) + # Check VCOL in Materials + for ma in d.materials: + # Cycles + if utils.cycles_exists(): + if ma and ma.node_tree and ma.node_tree.nodes: + for no in ma.node_tree.nodes: + if no and no.type in {'ATTRIBUTE'}: + if no.attribute_name == x: + objects = [] + + for ob in d.objects: + if ma.name in ob.material_slots: + objects.append(ob.name) + + if objects: + name = '{0} in object: {1}'.format(ma.name, objects) + else: + name = '{0} (unassigned)'.format(ma.name) + + if name not in AMTH_store_data.users['MATERIAL']: + AMTH_store_data.users['MATERIAL'].append(name) + + AMTH_store_data.libraries = sorted(list(set(AMTH_store_data.libraries))) + + # Print on console + empty = True + for t in AMTH_store_data.users: + if AMTH_store_data.users[t]: + empty = False + print('\n== {0} {1} use {2} "{3}" ==\n'.format( + len(AMTH_store_data.users[t]), + t, + dtype, + x)) + for p in AMTH_store_data.users[t]: + print(' {0}'.format(p)) + + if AMTH_store_data.libraries: + print_grammar("Check", "this library", "these libraries", + AMTH_store_data.libraries + ) + print_with_count_list(send_list=AMTH_store_data.libraries) + + if empty: + self.report({'INFO'}, "No users for {}".format(x)) + + return {"FINISHED"} + + +class AMTH_SCENE_OT_list_users_debug_clear(Operator): + """Clear the list bellow""" + bl_idname = "scene.amth_list_users_debug_clear" + bl_label = "Clear Debug Panel lists" + + what: StringProperty( + name="", + default="NONE", + options={'HIDDEN'} + ) + + def execute(self, context): + reset_global_storage(self.what) + + return {"FINISHED"} + + +class AMTH_SCENE_OT_blender_instance_open(Operator): + """Open in a new Blender instance""" + bl_idname = "scene.blender_instance_open" + bl_label = "Open Blender Instance" + + filepath: StringProperty() + + def execute(self, context): + if self.filepath: + filepath = os.path.normpath(bpy.path.abspath(self.filepath)) + + import subprocess + try: + subprocess.Popen([bpy.app.binary_path, filepath]) + except: + print("Error opening a new Blender instance") + import traceback + traceback.print_exc() + + return {"FINISHED"} + + +class AMTH_SCENE_OT_Collection_List_Refresh(Operator): + bl_idname = "scene.amaranth_lighters_corner_refresh" + bl_label = "Refresh" + bl_description = ("Generate/Refresh the Lists\n" + "Use to generate/refresh the list or after changes to Data") + bl_options = {"REGISTER", "INTERNAL"} + + what: StringProperty(default="NONE") + + def execute(self, context): + message = "No changes applied" + + if self.what == "LIGHTS": + fill_ligters_corner_props(context, refresh=True) + + found_lights = len(context.window_manager.amth_lighters_state.keys()) + message = "No Lights in the Data" if found_lights == 0 else \ + "Generated list for {} found light(s)".format(found_lights) + + elif self.what == "IMAGES": + fill_missing_images_props(context, refresh=True) + + found_images = len(context.window_manager.amth_missing_images_state.keys()) + message = "Great! No missing Images" if found_images == 0 else \ + "Missing {} image(s) in the Data".format(found_images) + + self.report({'INFO'}, message) + + return {"FINISHED"} + + +class AMTH_SCENE_PT_scene_debug(Panel): + """Scene Debug""" + bl_label = "Scene Debug" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {"DEFAULT_CLOSED"} + + def draw_header(self, context): + layout = self.layout + layout.label(text="", icon="RADIOBUT_ON") + + def draw_label(self, layout, body_text, single, multi, lists, ico="BLANK1"): + layout.label( + text="{} {} {}".format( + str(len(lists)), body_text, + single if len(lists) == 1 else multi), + icon=ico + ) + + def draw_miss_link(self, layout, text1, single, multi, text2, count, ico="BLANK1"): + layout.label( + text="{} {} {} {}".format( + count, text1, + single if count == 1 else multi, text2), + icon=ico + ) + + def draw(self, context): + layout = self.layout + scene = context.scene + + has_images = len(bpy.data.images) + engine = scene.render.engine + + # List Missing Images + box = layout.box() + split = box.split(factor=0.8, align=True) + row = split.row() + + if has_images: + subrow = split.row(align=True) + subrow.alignment = "RIGHT" + subrow.operator(AMTH_SCENE_OT_Collection_List_Refresh.bl_idname, + text="", icon="FILE_REFRESH").what = "IMAGES" + image_state = context.window_manager.amth_missing_images_state + + row.label( + text="{} Image Blocks present in the Data".format(has_images), + icon="IMAGE_DATA" + ) + if len(image_state.keys()) > 0: + box.template_list( + 'AMTH_UL_MissingImages_UI', + 'amth_collection_index_prop', + context.window_manager, + 'amth_missing_images_state', + context.window_manager.amth_collection_index_prop, + 'index_image', + rows=3 + ) + else: + row.label(text="No images loaded yet", icon="RIGHTARROW_THIN") + + # List Cycles Materials by Shader + if utils.cycles_exists() and engine == "CYCLES": + box = layout.box() + split = box.split() + col = split.column(align=True) + col.prop(scene, "amaranth_cycles_node_types", + icon="MATERIAL") + + row = split.row(align=True) + row.operator(AMTH_SCENE_OT_cycles_shader_list_nodes.bl_idname, + icon="SORTSIZE", + text="List Materials Using Shader") + if len(AMTH_store_data.mat_shaders) != 0: + row.operator( + AMTH_SCENE_OT_list_users_debug_clear.bl_idname, + icon="X", text="").what = "SHADER" + col.separator() + + if len(AMTH_store_data.mat_shaders) != 0: + col = box.column(align=True) + self.draw_label(col, "found", "material", "materials", + AMTH_store_data.mat_shaders, "INFO" + ) + for i, mat in enumerate(AMTH_store_data.mat_shaders): + col.label( + text="{}".format(AMTH_store_data.mat_shaders[i]), icon="MATERIAL" + ) + + # List Missing Node Trees + box = layout.box() + row = box.row(align=True) + split = row.split() + col = split.column(align=True) + + split = col.split(align=True) + split.label(text="Node Links") + row = split.row(align=True) + row.operator(AMTH_SCENE_OT_list_missing_node_links.bl_idname, + icon="NODETREE") + + if AMTH_store_data.count_groups != 0 or \ + AMTH_store_data.count_images != 0 or \ + AMTH_store_data.count_image_node_unlinked != 0: + + row.operator( + AMTH_SCENE_OT_list_users_debug_clear.bl_idname, + icon="X", text="").what = "NODE_LINK" + col.label(text="Warning! Check Console", icon="ERROR") + + if AMTH_store_data.count_groups != 0: + self.draw_miss_link(col, "node", "group", "groups", "missing link", + AMTH_store_data.count_groups, "NODE_TREE" + ) + if AMTH_store_data.count_images != 0: + self.draw_miss_link(col, "image", "node", "nodes", "missing link", + AMTH_store_data.count_images, "IMAGE_DATA" + ) + if AMTH_store_data.count_image_node_unlinked != 0: + self.draw_miss_link(col, "image", "node", "nodes", "with no output conected", + AMTH_store_data.count_image_node_unlinked, "NODE" + ) + + # List Empty Materials Slots + box = layout.box() + split = box.split() + col = split.column(align=True) + col.label(text="Material Slots") + + row = split.row(align=True) + row.operator(AMTH_SCENE_OT_list_missing_material_slots.bl_idname, + icon="MATERIAL", + text="List Empty Materials Slots" + ) + if len(AMTH_store_data.obj_mat_slots) != 0: + row.operator( + AMTH_SCENE_OT_list_users_debug_clear.bl_idname, + icon="X", text="").what = "MAT_SLOTS" + + col.separator() + col = box.column(align=True) + self.draw_label(col, "found empty material slot", "object", "objects", + AMTH_store_data.obj_mat_slots, "INFO" + ) + for entry, obs in enumerate(AMTH_store_data.obj_mat_slots): + row = col.row() + row.alignment = "LEFT" + row.label( + text="{}".format(AMTH_store_data.obj_mat_slots[entry]), + icon="OBJECT_DATA") + + if AMTH_store_data.obj_mat_slots_lib: + col.separator() + col.label("Check {}:".format( + "this library" if + len(AMTH_store_data.obj_mat_slots_lib) == 1 else + "these libraries") + ) + for ilib, libs in enumerate(AMTH_store_data.obj_mat_slots_lib): + row = col.row(align=True) + row.alignment = "LEFT" + row.operator( + AMTH_SCENE_OT_blender_instance_open.bl_idname, + text=AMTH_store_data.obj_mat_slots_lib[ilib], + icon="LINK_BLEND", + emboss=False).filepath = AMTH_store_data.obj_mat_slots_lib[ilib] + + box = layout.box() + row = box.row(align=True) + row.label(text="List Users for Datablock") + + col = box.column(align=True) + split = col.split() + row = split.row(align=True) + row.prop( + scene, "amth_datablock_types", + icon=scene.amth_datablock_types, + text="" + ) + row.operator_menu_enum( + "scene.amth_list_users_for_x_type", + "list_type_select", + text=scene.amth_list_users_for_x_name + ) + + row = split.row(align=True) + row.enabled = True if USER_X_NAME_EMPTY not in scene.amth_list_users_for_x_name else False + row.operator( + AMTH_SCENE_OT_list_users_for_x.bl_idname, + icon="COLLAPSEMENU").name = scene.amth_list_users_for_x_name + + if any(val for val in AMTH_store_data.users.values()): + col = box.column(align=True) + + for t in AMTH_store_data.users: + + for ma in AMTH_store_data.users[t]: + subrow = col.row(align=True) + subrow.alignment = "LEFT" + + if t == 'OBJECT_DATA': + text_lib = " [L] " if \ + ma in bpy.data.objects and bpy.data.objects[ma].library else "" + subrow.operator( + AMTH_SCENE_OT_amaranth_object_select.bl_idname, + text="{} {}{}".format(text_lib, ma, + "" if ma in context.scene.objects else " [Not in Scene]"), + icon=t, + emboss=False).object_name = ma + else: + subrow.label(text=ma, icon=t) + row.operator( + AMTH_SCENE_OT_list_users_debug_clear.bl_idname, + icon="X", text="").what = "XTYPE" + + if AMTH_store_data.libraries: + count_lib = 0 + + col.separator() + col.label("Check {}:".format( + "this library" if + len(AMTH_store_data.libraries) == 1 else + "these libraries") + ) + for libs in AMTH_store_data.libraries: + count_lib += 1 + row = col.row(align=True) + row.alignment = "LEFT" + row.operator( + AMTH_SCENE_OT_blender_instance_open.bl_idname, + text=AMTH_store_data.libraries[count_lib - 1], + icon="LINK_BLEND", + emboss=False).filepath = AMTH_store_data.libraries[count_lib - 1] + + +class AMTH_PT_LightersCorner(Panel): + """The Lighters Panel""" + bl_label = "Lighter's Corner" + bl_idname = "AMTH_SCENE_PT_lighters_corner" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "scene" + bl_options = {"DEFAULT_CLOSED"} + + def draw_header(self, context): + layout = self.layout + layout.label(text="", icon="LIGHT_SUN") + + def draw(self, context): + layout = self.layout + state_props = len(context.window_manager.amth_lighters_state) + engine = context.scene.render.engine + box = layout.box() + row = box.row(align=True) + + if utils.cycles_exists(): + row.prop(context.scene, "amaranth_lighterscorner_list_meshlights") + + subrow = row.row(align=True) + subrow.alignment = "RIGHT" + subrow.operator(AMTH_SCENE_OT_Collection_List_Refresh.bl_idname, + text="", icon="FILE_REFRESH").what = "LIGHTS" + + if not state_props: + row = box.row() + message = "Please Refresh" if len(bpy.data.lights) > 0 else "No Lights in Data" + row.label(text=message, icon="INFO") + else: + row = box.row(align=True) + split = row.split(factor=0.5, align=True) + col = split.column(align=True) + + col.label(text="Name/Library link") + + if engine in ["CYCLES", "BLENDER_RENDER"]: + splits = 0.6 if engine == "BLENDER_RENDER" else 0.4 + splita = split.split(factor=splits, align=True) + col = splita.column(align=True) + col.alignment = "LEFT" + col.label(text="Samples") + + if utils.cycles_exists() and engine == "CYCLES": + col = splita.column(align=True) + col.label(text="Size") + + cols = row.row(align=True) + cols.alignment = "RIGHT" + cols.label(text="{}Render Visibility/Selection".format( + "Rays /" if utils.cycles_exists() else "") + ) + box.template_list( + 'AMTH_UL_LightersCorner_UI', + 'amth_collection_index_prop', + context.window_manager, + 'amth_lighters_state', + context.window_manager.amth_collection_index_prop, + 'index', + rows=5 + ) + + +class AMTH_UL_MissingImages_UI(UIList): + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + text_lib = item.text_lib + has_filepath = item.has_filepath + is_library = item.is_library + + split = layout.split(factor=0.4) + row = split.row(align=True) + row.alignment = "LEFT" + row.label(text=text_lib, icon="IMAGE_DATA") + image = bpy.data.images.get(item.name, None) + + subrow = split.row(align=True) + splitp = subrow.split(factor=0.8, align=True).row(align=True) + splitp.alignment = "LEFT" + row_lib = subrow.row(align=True) + row_lib.alignment = "RIGHT" + if not image: + splitp.label(text="Image is not available", icon="ERROR") + else: + splitp.label(text=has_filepath, icon="LIBRARY_DATA_DIRECT") + if is_library: + row_lib.operator( + AMTH_SCENE_OT_blender_instance_open.bl_idname, + text="", + emboss=False, icon="LINK_BLEND").filepath = is_library + + +class AMTH_UL_LightersCorner_UI(UIList): + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + icon_type = item.icon_type + engine = context.scene.render.engine + text_lib = item.text_lib + is_library = item.is_library + + split = layout.split(factor=0.35) + row = split.row(align=True) + row.alignment = "LEFT" + row.label(text=text_lib, icon=icon_type) + ob = bpy.data.objects.get(item.name, None) + if not ob: + row.label(text="Object is not available", icon="ERROR") + else: + if is_library: + row.operator( + AMTH_SCENE_OT_blender_instance_open.bl_idname, + text="", + emboss=False, icon="LINK_BLEND").filepath = is_library + + rows = split.row(align=True) + splits = 0.9 if engine == "BLENDER_RENDER" else 0.4 + splitlamp = rows.split(factor=splits, align=True) + splitlampb = splitlamp.row(align=True) + splitlampc = splitlamp.row(align=True) + splitlampd = rows.row(align=True) + splitlampd.alignment = "RIGHT" + + if utils.cycles_exists() and engine == "CYCLES": + if "LIGHT" in icon_type: + clamp = ob.data.cycles + if context.scene.cycles.progressive == "BRANCHED_PATH": + splitlampb.prop(clamp, "samples", text="") + if context.scene.cycles.progressive == "PATH": + splitlampb.label(text="N/A") + lamp = ob.data + if lamp.type in ["POINT", "SUN", "SPOT"]: + splitlampc.label(text="{:.2f}".format(lamp.shadow_soft_size)) + elif lamp.type == "HEMI": + splitlampc.label(text="N/A") + elif lamp.type == "AREA" and lamp.shape == "RECTANGLE": + splitlampc.label( + text="{:.2f} x {:.2f}".format(lamp.size, lamp.size_y) + ) + else: + splitlampc.label(text="{:.2f}".format(lamp.size)) + else: + splitlampb.label(text="N/A") + if engine == "BLENDER_RENDER": + if "LIGHT" in icon_type: + lamp = ob.data + if lamp.type == "HEMI": + splitlampb.label(text="Not Available") + elif lamp.type == "AREA" and lamp.shadow_method == "RAY_SHADOW": + splitlampb.prop(lamp, "shadow_ray_samples_x", text="X") + if lamp.shape == "RECTANGLE": + splitlampb.prop(lamp, "shadow_ray_samples_y", text="Y") + elif lamp.shadow_method == "RAY_SHADOW": + splitlampb.prop(lamp, "shadow_ray_samples", text="Ray Samples") + elif lamp.shadow_method == "BUFFER_SHADOW": + splitlampb.prop(lamp, "shadow_buffer_samples", text="Buffer Samples") + else: + splitlampb.label(text="No Shadow") + else: + splitlampb.label(text="N/A") + if utils.cycles_exists(): + visibility = ob.cycles_visibility + splitlampd.prop(visibility, "camera", text="") + splitlampd.prop(visibility, "diffuse", text="") + splitlampd.prop(visibility, "glossy", text="") + splitlampd.prop(visibility, "shadow", text="") + splitlampd.separator() + splitlampd.prop(ob, "hide", text="", emboss=False) + splitlampd.prop(ob, "hide_render", text="", emboss=False) + splitlampd.operator( + AMTH_SCENE_OT_amaranth_object_select.bl_idname, + text="", + emboss=False, icon="RESTRICT_SELECT_OFF").object_name = item.name + + +def fill_missing_images_props(context, refresh=False): + image_state = context.window_manager.amth_missing_images_state + if refresh: + for key in image_state.keys(): + index = image_state.find(key) + if index != -1: + image_state.remove(index) + + for im in bpy.data.images: + if im.type not in ("UV_TEST", "RENDER_RESULT", "COMPOSITING"): + if not im.packed_file and \ + not os.path.exists(bpy.path.abspath(im.filepath, library=im.library)): + text_l = "{}{} [{}]{}".format("[L] " if im.library else "", im.name, + im.users, " [F]" if im.use_fake_user else "") + prop = image_state.add() + prop.name = im.name + prop.text_lib = text_l + prop.has_filepath = im.filepath if im.filepath else "No Filepath" + prop.is_library = im.library.filepath if im.library else "" + + +def fill_ligters_corner_props(context, refresh=False): + light_state = context.window_manager.amth_lighters_state + list_meshlights = context.scene.amaranth_lighterscorner_list_meshlights + if refresh: + for key in light_state.keys(): + index = light_state.find(key) + if index != -1: + light_state.remove(index) + + for ob in bpy.data.objects: + if ob.name not in light_state.keys() or refresh: + is_light = ob.type == "LIGHT" + is_emission = True if utils.cycles_is_emission( + context, ob) and list_meshlights else False + + if is_light or is_emission: + icons = "LIGHT_%s" % ob.data.type if is_light else "MESH_GRID" + text_l = "{} {}{}".format(" [L] " if ob.library else "", ob.name, + "" if ob.name in context.scene.objects else " [Not in Scene]") + prop = light_state.add() + prop.name = ob.name + prop.icon_type = icons + prop.text_lib = text_l + prop.is_library = ob.library.filepath if ob.library else "" + + +class AMTH_LightersCornerStateProp(PropertyGroup): + icon_type: StringProperty() + text_lib: StringProperty() + is_library: StringProperty() + + +class AMTH_MissingImagesStateProp(PropertyGroup): + text_lib: StringProperty() + has_filepath: StringProperty() + is_library: StringProperty() + + +class AMTH_LightersCollectionIndexProp(PropertyGroup): + index: IntProperty( + name="index" + ) + index_image: IntProperty( + name="index" + ) + + +classes = ( + AMTH_SCENE_PT_scene_debug, + AMTH_SCENE_OT_list_users_debug_clear, + AMTH_SCENE_OT_blender_instance_open, + AMTH_SCENE_OT_amaranth_object_select, + AMTH_SCENE_OT_list_missing_node_links, + AMTH_SCENE_OT_list_missing_material_slots, + AMTH_SCENE_OT_cycles_shader_list_nodes, + AMTH_SCENE_OT_list_users_for_x, + AMTH_SCENE_OT_list_users_for_x_type, + AMTH_SCENE_OT_Collection_List_Refresh, + AMTH_LightersCornerStateProp, + AMTH_LightersCollectionIndexProp, + AMTH_MissingImagesStateProp, + AMTH_PT_LightersCorner, + AMTH_UL_LightersCorner_UI, + AMTH_UL_MissingImages_UI, +) + + +def register(): + init() + + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.amth_list_users_for_x_name = StringProperty( + default="Select DataBlock Name", + name="Name", + description=USER_X_NAME_EMPTY, + options={"SKIP_SAVE"} + ) + bpy.types.WindowManager.amth_collection_index_prop = PointerProperty( + type=AMTH_LightersCollectionIndexProp + ) + bpy.types.WindowManager.amth_lighters_state = CollectionProperty( + type=AMTH_LightersCornerStateProp + ) + bpy.types.WindowManager.amth_missing_images_state = CollectionProperty( + type=AMTH_MissingImagesStateProp + ) + + +def unregister(): + clear() + + for cls in classes: + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.amth_list_users_for_x_name + del bpy.types.WindowManager.amth_collection_index_prop + del bpy.types.WindowManager.amth_lighters_state + del bpy.types.WindowManager.amth_missing_images_state diff --git a/amaranth/scene/goto_library.py b/amaranth/scene/goto_library.py new file mode 100644 index 00000000..08f1689c --- /dev/null +++ b/amaranth/scene/goto_library.py @@ -0,0 +1,90 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +File Browser: Libraries Bookmark + +The "Libraries" panel on the File Browser displays the path to all the +libraries linked to that .blend. So you can quickly go to the folders +related to the file. + +Click on any path to go to that directory. +Developed during Caminandes Open Movie Project +""" + +import bpy + + +class AMTH_FILE_PT_libraries(bpy.types.Panel): + bl_space_type = "FILE_BROWSER" + bl_region_type = "TOOLS" + bl_category = "Bookmarks" + bl_label = "Libraries" + + def draw(self, context): + layout = self.layout + + libs = bpy.data.libraries + libslist = [] + + # Build the list of folders from libraries + import os.path + + for lib in libs: + directory_name = os.path.dirname(lib.filepath) + libslist.append(directory_name) + + # Remove duplicates and sort by name + libslist = set(libslist) + libslist = sorted(libslist) + + # Draw the box with libs + row = layout.row() + box = row.box() + + if libslist: + col = box.column() + for filepath in libslist: + if filepath != "//": + row = col.row() + row.alignment = "LEFT" + props = row.operator( + AMTH_FILE_OT_directory_go_to.bl_idname, + text=filepath, icon="BOOKMARKS", + emboss=False) + props.filepath = filepath + else: + box.label(text="No libraries loaded") + + +class AMTH_FILE_OT_directory_go_to(bpy.types.Operator): + + """Go to this library"s directory""" + bl_idname = "file.directory_go_to" + bl_label = "Go To" + + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + bpy.ops.file.select_bookmark(dir=self.filepath) + return {"FINISHED"} + + +def register(): + bpy.utils.register_class(AMTH_FILE_PT_libraries) + bpy.utils.register_class(AMTH_FILE_OT_directory_go_to) + + +def unregister(): + bpy.utils.unregister_class(AMTH_FILE_PT_libraries) + bpy.utils.unregister_class(AMTH_FILE_OT_directory_go_to) diff --git a/amaranth/scene/material_remove_unassigned.py b/amaranth/scene/material_remove_unassigned.py new file mode 100644 index 00000000..f19bc3d2 --- /dev/null +++ b/amaranth/scene/material_remove_unassigned.py @@ -0,0 +1,108 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +import bpy + + +# FEATURE: Delete Materials not assigned to any verts +class AMTH_OBJECT_OT_material_remove_unassigned(bpy.types.Operator): + + """Remove materials not assigned to any vertex""" + bl_idname = "object.amaranth_object_material_remove_unassigned" + bl_label = "Remove Unassigned Materials" + + @classmethod + def poll(cls, context): + return context.active_object.material_slots + + def execute(self, context): + + scene = context.scene + act_ob = context.active_object + count = len(act_ob.material_slots) + materials_removed = [] + act_ob.active_material_index = 0 + is_visible = True + + if act_ob not in context.visible_objects: + is_visible = False + n = -1 + for lay in act_ob.layers: + n += 1 + if lay: + break + + scene.layers[n] = True + + for slot in act_ob.material_slots: + count -= 1 + + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_all(action="DESELECT") + act_ob.active_material_index = count + bpy.ops.object.material_slot_select() + + if act_ob.data.total_vert_sel == 0 or \ + (len(act_ob.material_slots) == 1 and not + act_ob.material_slots[0].material): + materials_removed.append( + "%s" % + act_ob.active_material.name if act_ob.active_material else "Empty") + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.material_slot_remove() + else: + pass + + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_all(action="DESELECT") + bpy.ops.object.mode_set(mode="OBJECT") + + if materials_removed: + print( + "\n* Removed %s Unassigned Materials \n" % + len(materials_removed)) + + count_mr = 0 + + for mr in materials_removed: + count_mr += 1 + print( + "%0.2d. %s" % + (count_mr, materials_removed[count_mr - 1])) + + print("\n") + self.report({"INFO"}, "Removed %s Unassigned Materials" % + len(materials_removed)) + + if not is_visible: + scene.layers[n] = False + + return {"FINISHED"} + + +def ui_material_remove_unassigned(self, context): + self.layout.operator( + AMTH_OBJECT_OT_material_remove_unassigned.bl_idname, + icon="X") + +# // FEATURE: Delete Materials not assigned to any verts + + +def register(): + bpy.utils.register_class(AMTH_OBJECT_OT_material_remove_unassigned) + bpy.types.MATERIAL_MT_context_menu.append(ui_material_remove_unassigned) + + +def unregister(): + bpy.utils.unregister_class(AMTH_OBJECT_OT_material_remove_unassigned) + bpy.types.MATERIAL_MT_context_menu.remove(ui_material_remove_unassigned) diff --git a/amaranth/scene/refresh.py b/amaranth/scene/refresh.py new file mode 100644 index 00000000..3ef5e6b7 --- /dev/null +++ b/amaranth/scene/refresh.py @@ -0,0 +1,76 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Refresh Scene + +Refresh the current scene, useful when working with libraries or drivers. +Could also add an option to refresh the VSE maybe? Usage: Hit F5 or find +it on the Specials menu W. +""" + +import bpy + + +KEYMAPS = list() + + +class AMTH_SCENE_OT_refresh(bpy.types.Operator): + """Refresh the current scene""" + bl_idname = "scene.refresh" + bl_label = "Refresh!" + + def execute(self, context): + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return {"CANCELLED"} + + preferences = context.preferences.addons["amaranth"].preferences + scene = context.scene + + if preferences.use_scene_refresh: + # Changing the frame is usually the best way to go + scene.frame_current = scene.frame_current + self.report({"INFO"}, "Scene Refreshed!") + + return {"FINISHED"} + + +def button_refresh(self, context): + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return + + if context.preferences.addons["amaranth"].preferences.use_scene_refresh: + self.layout.separator() + self.layout.operator(AMTH_SCENE_OT_refresh.bl_idname, + text="Refresh!", + icon="FILE_REFRESH") + + +def register(): + bpy.utils.register_class(AMTH_SCENE_OT_refresh) + bpy.types.VIEW3D_MT_object_context_menu.append(button_refresh) + kc = bpy.context.window_manager.keyconfigs.addon + km = kc.keymaps.new(name="Window") + kmi = km.keymap_items.new("scene.refresh", "F5", "PRESS", + alt=True) + KEYMAPS.append((km, kmi)) + + +def unregister(): + bpy.utils.unregister_class(AMTH_SCENE_OT_refresh) + bpy.types.VIEW3D_MT_object_context_menu.remove(button_refresh) + for km, kmi in KEYMAPS: + km.keymap_items.remove(kmi) + KEYMAPS.clear() diff --git a/amaranth/scene/save_reload.py b/amaranth/scene/save_reload.py new file mode 100644 index 00000000..7b961b64 --- /dev/null +++ b/amaranth/scene/save_reload.py @@ -0,0 +1,78 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Save & Reload File + +When working with linked libraries, very often you need to save and load +again to see the changes. +This does it in one go, without asking, so be careful :) +Usage: Hit Ctrl + Shift + W or find it at the bottom of the File menu. +""" + +import bpy + + +KEYMAPS = list() + + +class AMTH_WM_OT_save_reload(bpy.types.Operator): + """Save and Reload the current blend file""" + bl_idname = "wm.save_reload" + bl_label = "Save & Reload" + + def save_reload(self, context, path): + if not path: + bpy.ops.wm.save_as_mainfile("INVOKE_AREA") + return + bpy.ops.wm.save_mainfile() + self.report({"INFO"}, "Saved & Reloaded") + bpy.ops.wm.open_mainfile("EXEC_DEFAULT", filepath=path) + + def execute(self, context): + path = bpy.data.filepath + self.save_reload(context, path) + + return {"FINISHED"} + + +def button_save_reload(self, context): + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return + + if context.preferences.addons["amaranth"].preferences.use_file_save_reload: + self.layout.separator() + self.layout.operator( + AMTH_WM_OT_save_reload.bl_idname, + text="Save & Reload", + icon="FILE_REFRESH") + + +def register(): + bpy.utils.register_class(AMTH_WM_OT_save_reload) + bpy.types.TOPBAR_MT_file.append(button_save_reload) + wm = bpy.context.window_manager + kc = wm.keyconfigs.addon + km = kc.keymaps.new(name="Window") + kmi = km.keymap_items.new("wm.save_reload", "W", "PRESS", + shift=True, ctrl=True) + KEYMAPS.append((km, kmi)) + + +def unregister(): + bpy.utils.unregister_class(AMTH_WM_OT_save_reload) + bpy.types.TOPBAR_MT_file.remove(button_save_reload) + for km, kmi in KEYMAPS: + km.keymap_items.remove(kmi) + KEYMAPS.clear() diff --git a/amaranth/scene/stats.py b/amaranth/scene/stats.py new file mode 100644 index 00000000..10d8be44 --- /dev/null +++ b/amaranth/scene/stats.py @@ -0,0 +1,62 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Scene, Cameras, and Meshlights Count + +Increase the stats by displaying the number of scenes, cameras, and light +emitting meshes. +On the Info header. +""" + +import bpy +from amaranth import utils + + +def stats_scene(self, context): + get_addon = "amaranth" in context.preferences.addons.keys() + if not get_addon: + return + + if context.preferences.addons["amaranth"].preferences.use_scene_stats: + scenes_count = str(len(bpy.data.scenes)) + cameras_count = str(len(bpy.data.cameras)) + cameras_selected = 0 + meshlights = 0 + meshlights_visible = 0 + + for ob in context.scene.objects: + if utils.cycles_is_emission(context, ob): + meshlights += 1 + if ob in context.visible_objects: + meshlights_visible += 1 + + if ob in context.selected_objects: + if ob.type == 'CAMERA': + cameras_selected += 1 + + meshlights_string = '| Meshlights:{}/{}'.format( + meshlights_visible, meshlights) + + row = self.layout.row(align=True) + row.label(text="Scenes:{} | Cameras:{}/{} {}".format( + scenes_count, cameras_selected, cameras_count, + meshlights_string if utils.cycles_active(context) else '')) + + +def register(): + bpy.types.STATUSBAR_HT_header.append(stats_scene) + + +def unregister(): + bpy.types.STATUSBAR_HT_header.remove(stats_scene) diff --git a/amaranth/utils.py b/amaranth/utils.py new file mode 100644 index 00000000..f6367875 --- /dev/null +++ b/amaranth/utils.py @@ -0,0 +1,64 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import bpy + + +# FUNCTION: Checks if cycles is available +def cycles_exists(): + return hasattr(bpy.types.Scene, "cycles") + + +# FUNCTION: Checks if cycles is the active renderer +def cycles_active(context): + return context.scene.render.engine == "CYCLES" + + +# FUNCTION: Check if material has Emission (for select and stats) +def cycles_is_emission(context, ob): + is_emission = False + + if not ob.material_slots: + return is_emission + + for ma in ob.material_slots: + if not ma.material: + continue + if ma.material.node_tree and ma.material.node_tree.nodes: + for no in ma.material.node_tree.nodes: + if not no.type in ("EMISSION", "GROUP"): + continue + for ou in no.outputs: + if not ou.links: + continue + if no.type == "GROUP" and no.node_tree and no.node_tree.nodes: + for gno in no.node_tree.nodes: + if gno.type != "EMISSION": + continue + for gou in gno.outputs: + if ou.links and gou.links: + is_emission = True + elif no.type == "EMISSION": + if ou.links: + is_emission = True + return is_emission + + +# FUNCTION: Check if object has keyframes for a specific frame +def is_keyframe(ob, frame): + if ob is not None and ob.animation_data is not None and ob.animation_data.action is not None: + for fcu in ob.animation_data.action.fcurves: + if frame in (p.co.x for p in fcu.keyframe_points): + return True + return False |