├── .gitignore ├── README.md ├── __init__.py ├── bakelist.py ├── baker.py ├── check_path_access.py ├── combinelist.py ├── const.py ├── duplicate.py ├── functions.py ├── image ├── combine.py ├── invert.py ├── prefix.py ├── save.py ├── save_as.py └── suffix.py ├── joblist.py ├── material ├── add_images.py ├── add_temp_material.py ├── delete_tagged_materials.py ├── has_material.py └── new.py ├── nodes ├── delete_tagged.py ├── duplicate.py ├── find.py ├── new.py ├── node.py ├── outputs.py ├── principled_node.py ├── socket_index.py ├── ungroup.py └── value_list.py ├── panel.py ├── prefs.py ├── prepare ├── material.py └── objects.py ├── presets.py ├── set_samples.py ├── settings.py ├── suffixlist.py └── uv ├── project.py └── select.py /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | *.txt 3 | __pycache__/ 4 | /.vscode 5 | TODO 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Principled-Baker 2 | A Blender Add-on: Bake PBR textures with a few clicks 3 | --- 4 | 5 | [Principled Baker for Blender 2.79](https://github.com/danielenger/Principled-Baker_for_2-79) 6 | 7 | 8 | Features: 9 | -- 10 | - Autodetection of what needs to be baked by connected inputs 11 | - Manual selection for texture channels 12 | - bake almost all Principled BSDF (and more) inputs (Color, Metallic, Roughness, etc.) to image textures 13 | - Autodetection/Manual selection also for Alpha, Emission, Ambient Occlusion (from node; 2.80 only), Diffuse, Glossiness (invert Roughness), Bump (as hightmap), Vertex Color, Material ID 14 | - 3 Bake Modes: 15 | - Combined: Bake a single selected object or bake multiple selected objects with shared UV maps. This is like Blenders default bake. 16 | - Single/Batch: Bake every selected object separately. 17 | - Selected to Active: Does what it says. 18 | - Create new material with new image texture nodes (most image nodes connected) 19 | - Auto Smooth from object/on/off 20 | - Auto UV unwrap option: Smart UV Project/Lightmap Pack 21 | 22 | --- 23 | Limitations/Warnings: 24 | -- 25 | - **Be careful with Overwrite! It does what it says!** 26 | 27 | - Baking works in Cycles only. (see preferences Bake "in" Eevee) 28 | 29 | - Displacement works only with a Displacement node. (Blender 2.80) 30 | Vector Displacement does not work. 31 | 32 | - Color inputs of transparent nodes (Transparent, Translucent, Glass) will be ignored by default. 33 | This prevents false colors at transitions from being baked into the Color Texture. 34 | Deactivate "Exclude Transparent Colors" to bake transparent inputs to Color Texture. 35 | 36 | - Autodetection: 37 | If just a Bump node is in the node tree, the Normal Map will always be baked. 38 | If a Normal Map and a Bump Map is baked, the Bump node will not be linked in newly created material. 39 | 40 | - Some results from complex mixed shader node trees might not be useful 41 | 42 | - with Material Name to define the Material ID Colors: Duplicate colors are possible! 43 | 44 | - Baking "in" Eevee might crash Blender! 45 | 46 | - known issues: 47 | * results for Subsurface Radius is not useful 48 | * results for Tangent might not be useful 49 | * batch baking with shared materials can give useless, half baked image textures 50 | * typo in github name (can not be solved?) 51 | 52 | 53 | *** 54 | Thread on blenderartists: 55 | https://blenderartists.org/t/addon-principled-baker/1102187 56 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .bakelist import * 4 | from .baker import PBAKER_OT_bake 5 | from .combinelist import * 6 | from .panel import * 7 | from .prefs import PBAKER_prefs 8 | from .presets import * 9 | from .settings import PBAKER_settings 10 | from .suffixlist import * 11 | 12 | bl_info = { 13 | "name": "Principled Baker", 14 | "description": "bakes all inputs of Principled BSDF to image textures", 15 | "author": "Daniel Engler", 16 | "version": (0, 5, 7), 17 | "blender": (2, 83, 0), 18 | "location": "Shader Editor Toolbar", 19 | "category": "Node", 20 | } 21 | 22 | classes = ( 23 | PBAKER_OT_bake, 24 | PBAKER_prefs, 25 | PBAKER_settings, 26 | PBAKER_UL_List, 27 | PBAKER_ListItem, 28 | PBAKER_BAKELIST_OT_Init, 29 | PBAKER_BAKELIST_OT_Update, 30 | PBAKER_BAKELIST_OT_Detect, 31 | PBAKER_BAKELIST_OT_Delete, 32 | PBAKER_BAKELIST_OT_Reset, 33 | PBAKER_BAKELIST_OT_Disable_All, 34 | PBAKER_BAKELIST_OT_MoveItem_Up, 35 | PBAKER_BAKELIST_OT_MoveItem_Down, 36 | PBAKER_UL_SuffixList, 37 | PBAKER_SuffixListItem, 38 | PBAKER_SUFFIXLIST_OT_Init, 39 | PBAKER_SUFFIXLIST_OT_Delete, 40 | PBAKER_SUFFIXLIST_OT_Reset, 41 | PBAKER_UL_CombineList, 42 | PBAKER_CombineListItem, 43 | PBAKER_COMBINELIST_OT_Add, 44 | PBAKER_COMBINELIST_OT_Delete, 45 | PBAKER_COMBINELIST_OT_MoveItem_Up, 46 | PBAKER_COMBINELIST_OT_MoveItem_Down, 47 | PBAKER_AddPresetObjectDisplay, 48 | PBAKER_MT_display_presets, 49 | PBAKER_AddSuffixPresetObjectDisplay, 50 | PBAKER_MT_display_suffix_presets, 51 | PBAKER_AddCombinePresetObjectDisplay, 52 | PBAKER_MT_display_combine_presets, 53 | PBAKER_PT_Main, 54 | PBAKER_PT_SubPanel, 55 | PBAKER_PT_BakeList, 56 | PBAKER_PT_AdditionalBakeTypes, 57 | PBAKER_PT_OutputSettings, 58 | PBAKER_PT_SelectedToActiveSettings, 59 | PBAKER_PT_NewMaterial, 60 | PBAKER_PT_SelectUVMap, 61 | PBAKER_PT_AutoUVUnwrap, 62 | PBAKER_PT_AutoSmooth, 63 | PBAKER_PT_CombineChannels, 64 | PBAKER_PT_DuplicateObjects, 65 | PBAKER_PT_PrefixSuffixSettings, 66 | PBAKER_PT_Misc, 67 | ) 68 | 69 | 70 | def register(): 71 | for cls in classes: 72 | bpy.utils.register_class(cls) 73 | 74 | bpy.types.Scene.principled_baker_settings = bpy.props.PointerProperty( 75 | type=PBAKER_settings) 76 | 77 | bpy.types.Scene.principled_baker_bakelist = bpy.props.CollectionProperty( 78 | type=PBAKER_ListItem) 79 | bpy.types.Scene.principled_baker_bakelist_index = bpy.props.IntProperty( 80 | name="Bakelist Index", default=0) 81 | 82 | bpy.types.Scene.principled_baker_suffixlist = bpy.props.CollectionProperty( 83 | type=PBAKER_SuffixListItem) 84 | bpy.types.Scene.principled_baker_suffixlist_index = bpy.props.IntProperty( 85 | name="Suffixlist Index", default=0) 86 | 87 | bpy.types.Scene.principled_baker_combinelist = bpy.props.CollectionProperty( 88 | type=PBAKER_CombineListItem) 89 | bpy.types.Scene.principled_baker_combinelist_index = bpy.props.IntProperty( 90 | name="Combinelist Index", default=0) 91 | 92 | 93 | def unregister(): 94 | for cls in reversed(classes): 95 | bpy.utils.unregister_class(cls) 96 | 97 | del bpy.types.Scene.principled_baker_settings 98 | del bpy.types.Scene.principled_baker_bakelist_index 99 | del bpy.types.Scene.principled_baker_suffixlist_index 100 | del bpy.types.Scene.principled_baker_combinelist_index 101 | 102 | 103 | if __name__ == "__main__": 104 | register() 105 | 106 | 107 | # Principled Baker 108 | # Copyright (C) 2018-2020 Daniel Engler 109 | 110 | # This program is free software: you can redistribute it and/or modify 111 | # it under the terms of the GNU General Public License as published by 112 | # the Free Software Foundation, either version 3 of the License, or 113 | # (at your option) any later version. 114 | 115 | # This program is distributed in the hope that it will be useful, 116 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 117 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 118 | # GNU General Public License for more details. 119 | 120 | # You should have received a copy of the GNU General Public License 121 | # along with this program. If not, see . 122 | -------------------------------------------------------------------------------- /bakelist.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty, EnumProperty, IntProperty 3 | from bpy.types import Operator, PropertyGroup, UIList 4 | 5 | from .joblist import get_joblist_by_autodetection_from_objects 6 | 7 | DEFAULT_SAMPLES = 128 8 | 9 | JOBLIST = [ 10 | "Color", 11 | "Metallic", 12 | "Roughness", 13 | 14 | "Normal", 15 | # "Bump", 16 | "Displacement", 17 | 18 | "Alpha", 19 | "Emission", 20 | 'Ambient Occlusion', 21 | 22 | "Subsurface", 23 | "Subsurface Radius", 24 | "Subsurface Color", 25 | "Specular", 26 | "Specular Tint", 27 | "Anisotropic", 28 | "Anisotropic Rotation", 29 | "Sheen", 30 | "Sheen Tint", 31 | "Clearcoat", 32 | "Clearcoat Roughness", 33 | "IOR", 34 | "Transmission", 35 | "Transmission Roughness", 36 | "Clearcoat Normal", 37 | "Tangent", 38 | ] 39 | 40 | JOBLIST_SHORT = [ 41 | "Color", 42 | "Metallic", 43 | "Roughness", 44 | "Normal", 45 | "Displacement", 46 | "Alpha", 47 | "Emission", 48 | 'Ambient Occlusion', 49 | ] 50 | 51 | # temp store for values in bake list to toggle short/long bake list 52 | temp_bakelist = {} 53 | 54 | 55 | def color_mode_items(item, context): 56 | if bpy.context.scene.principled_baker_settings.file_format in ['PNG', 'TARGA', 'TIFF', 'OPEN_EXR']: 57 | items = [ 58 | ('RGB', "RGB", ""), 59 | ('RGBA', "RGBA", ""), 60 | ] 61 | else: 62 | items = [ 63 | ('RGB', "RGB", ""), 64 | ('BW', "BW", ""), 65 | ] 66 | return items 67 | 68 | 69 | def color_depth_items(scene, context): 70 | if bpy.context.scene.principled_baker_settings.file_format == 'OPEN_EXR': 71 | items = [ 72 | ('16', "Float (Half)", ""), 73 | ('32', "Float (Full)", "") 74 | ] 75 | else: 76 | items = [ 77 | ('8', "8", ""), 78 | ('16', "16", ""), 79 | ] 80 | return items 81 | 82 | 83 | class PBAKER_ListItem(PropertyGroup): 84 | do_bake: BoolProperty( 85 | name="", 86 | default=False 87 | ) 88 | # color_mode: EnumProperty( 89 | # name="Color Mode", 90 | # description="Color Mode", 91 | # items=color_mode_items 92 | # ) 93 | color_depth: EnumProperty( 94 | name="Color Depth", 95 | description="Color Depth", 96 | items=color_depth_items 97 | ) 98 | samples: IntProperty( 99 | name="Samples", 100 | default=128, 101 | min=1 102 | ) 103 | 104 | 105 | class PBAKER_UL_List(UIList): 106 | def draw_item(self, context, layout, data, item, icon, active_data, 107 | active_propname, index): 108 | 109 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 110 | layout.prop(item, "do_bake") 111 | layout.label(text=item.name) 112 | 113 | settings = bpy.context.scene.principled_baker_settings 114 | 115 | if settings.individual_samples: 116 | layout.prop(item, "samples", text="") 117 | 118 | if settings.color_depth == 'INDIVIDUAL': 119 | layout.prop(item, "color_depth", expand=True) 120 | 121 | # TODO expand bake list 122 | # layout.prop(item, "color_mode", expand=True) # is this useful? 123 | 124 | 125 | class PBAKER_BAKELIST_OT_Init(Operator): 126 | bl_idname = "principled_baker_bakelist.init" 127 | bl_label = "Init Bakelist" 128 | 129 | def execute(self, context): 130 | bake_list = context.scene.principled_baker_bakelist 131 | 132 | if not len(bake_list): 133 | for jobname in JOBLIST: 134 | if bpy.context.scene.principled_baker_settings.use_shortlist: 135 | if jobname in JOBLIST_SHORT: 136 | item = bake_list.add() 137 | item.name = jobname 138 | else: 139 | item = bake_list.add() 140 | item.name = jobname 141 | item.samples = DEFAULT_SAMPLES 142 | 143 | return{'FINISHED'} 144 | 145 | 146 | class PBAKER_BAKELIST_OT_Delete(Operator): 147 | bl_idname = "principled_baker_bakelist.delete" 148 | bl_label = "Delete Bakelist" 149 | 150 | @classmethod 151 | def poll(cls, context): 152 | return context.scene.principled_baker_bakelist 153 | 154 | def execute(self, context): 155 | context.scene.principled_baker_bakelist.clear() 156 | return{'FINISHED'} 157 | 158 | 159 | class PBAKER_BAKELIST_OT_Update(Operator): 160 | bl_idname = "principled_baker_bakelist.update" 161 | bl_label = "Update Bakelist" 162 | 163 | def execute(self, context): 164 | 165 | bakelist = context.scene.principled_baker_bakelist 166 | 167 | for item_name, item in bakelist.items(): 168 | temp_bakelist[item_name] = (item.do_bake, item.samples) 169 | 170 | bpy.ops.principled_baker_bakelist.reset() 171 | 172 | for item_name, item in bakelist.items(): 173 | item.do_bake = temp_bakelist[item_name][0] 174 | item.samples = temp_bakelist[item_name][1] 175 | 176 | return{'FINISHED'} 177 | 178 | 179 | class PBAKER_BAKELIST_OT_Detect(Operator): 180 | """Detect Bake Types from Selected Objects""" 181 | 182 | bl_idname = "principled_baker_bakelist.detect" 183 | bl_label = "Update Bakelist" 184 | 185 | def execute(self, context): 186 | settings = bpy.context.scene.principled_baker_settings 187 | 188 | if not settings.use_value_differ and not settings.use_connected_inputs: 189 | self.report({'INFO'}, "Select at least one Detection Option!") 190 | return {'CANCELLED'} 191 | 192 | if not context.scene.principled_baker_bakelist: 193 | bpy.ops.principled_baker_bakelist.init() 194 | 195 | bakelist = context.scene.principled_baker_bakelist 196 | 197 | temp_joblist = get_joblist_by_autodetection_from_objects( 198 | context.selected_objects) 199 | 200 | for item_name, item in bakelist.items(): 201 | if item_name in temp_joblist: 202 | item.do_bake = True 203 | else: 204 | item.do_bake = False 205 | 206 | return {'FINISHED'} 207 | 208 | 209 | class PBAKER_BAKELIST_OT_Reset(Operator): 210 | bl_idname = "principled_baker_bakelist.reset" 211 | bl_label = "Update Bakelist" 212 | 213 | def execute(self, context): 214 | if context.scene.principled_baker_bakelist: 215 | bpy.ops.principled_baker_bakelist.delete() 216 | bpy.ops.principled_baker_bakelist.init() 217 | 218 | return{'FINISHED'} 219 | 220 | 221 | class PBAKER_BAKELIST_OT_Disable_All(Operator): 222 | bl_idname = "principled_baker_bakelist.disable_all" 223 | bl_label = "Disable All" 224 | 225 | def execute(self, context): 226 | bakelist = context.scene.principled_baker_bakelist 227 | 228 | for _, item in bakelist.items(): 229 | item.do_bake = False 230 | 231 | return{'FINISHED'} 232 | 233 | 234 | class PBAKER_BAKELIST_OT_MoveItem_Up(Operator): 235 | bl_idname = "principled_baker_bakelist.move_up" 236 | bl_label = "Up" 237 | 238 | @classmethod 239 | def poll(cls, context): 240 | return context.scene.principled_baker_bakelist 241 | 242 | def execute(self, context): 243 | principled_baker_bakelist = context.scene.principled_baker_bakelist 244 | index = bpy.context.scene.principled_baker_bakelist_index 245 | prev_index = index - 1 246 | principled_baker_bakelist.move(prev_index, index) 247 | new_index = index - 1 248 | list_len = len(principled_baker_bakelist) 249 | bpy.context.scene.principled_baker_bakelist_index = max( 250 | 0, min(new_index, list_len - 1)) 251 | 252 | return{'FINISHED'} 253 | 254 | 255 | class PBAKER_BAKELIST_OT_MoveItem_Down(Operator): 256 | bl_idname = "principled_baker_bakelist.move_down" 257 | bl_label = "Down" 258 | 259 | @classmethod 260 | def poll(cls, context): 261 | return context.scene.principled_baker_bakelist 262 | 263 | def execute(self, context): 264 | principled_baker_bakelist = context.scene.principled_baker_bakelist 265 | index = bpy.context.scene.principled_baker_bakelist_index 266 | next_index = index + 1 267 | principled_baker_bakelist.move(next_index, index) 268 | new_index = index + 1 269 | list_len = len(principled_baker_bakelist) 270 | bpy.context.scene.principled_baker_bakelist_index = max( 271 | 0, min(new_index, list_len - 1)) 272 | 273 | return{'FINISHED'} 274 | -------------------------------------------------------------------------------- /baker.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import bpy 4 | 5 | from .check_path_access import check_path_access 6 | from .const import (ALPHA_NODES, IMAGE_FILE_FORMAT_ENDINGS, MATERIAL_TAG, 7 | MATERIAL_TAG_VERTEX, NODE_INPUTS, NODE_INPUTS_SORTED, 8 | NODE_TAG, NORMAL_INPUTS) 9 | from .duplicate import * 10 | from .functions import (get_bake_type_by, get_only_meshes, is_list_equal, 11 | remove_not_allowed_signs) 12 | from .image.combine import combine_channels_to_image, get_combined_images 13 | from .image.invert import get_invert_image 14 | from .image.prefix import get_image_prefix 15 | from .image.save import save_image 16 | from .image.save_as import save_image_as 17 | from .image.suffix import get_image_suffix 18 | from .joblist import (get_joblist_from_objects, 19 | get_vertex_colors_to_bake_from_objects) 20 | from .material.add_images import add_images_to_material 21 | from .material.add_temp_material import add_temp_material 22 | from .material.delete_tagged_materials import delete_tagged_materials 23 | from .material.has_material import has_material 24 | from .material.new import new_material 25 | from .nodes.delete_tagged import delete_tagged_nodes_in_object 26 | from .nodes.find import find_node_by_type 27 | from .nodes.new import (create_bake_image_nodes, new_image_node, 28 | new_mixrgb_node, new_pb_emission_node, 29 | new_pb_output_node) 30 | from .nodes.node import get_sibling_node, is_node_type_in_node_tree 31 | from .nodes.outputs import (deactivate_material_outputs, get_active_output, 32 | get_active_outputs, get_all_material_outputs, 33 | set_material_outputs_target_to_all) 34 | from .nodes.principled_node import get_principled_node_values 35 | from .prepare.objects import (prepare_objects_for_bake, 36 | prepare_objects_for_bake_matid, 37 | prepare_objects_for_bake_vertex_color, 38 | prepare_objects_for_bake_wireframe) 39 | from .set_samples import set_samples 40 | from .uv.project import * 41 | from .uv.select import * 42 | 43 | 44 | class PBAKER_OT_bake(bpy.types.Operator): 45 | bl_idname = "object.principled_baker_bake" 46 | bl_label = "Bake" 47 | bl_description = "bake all inputs of a Principled BSDF to image textures" 48 | bl_options = {'REGISTER', 'UNDO'} 49 | 50 | def load_image_by(self, image_file_name) -> bpy.types.Image: 51 | """:returns: Reference to image.""" 52 | 53 | path = self.get_image_file_path(image_file_name) 54 | image = bpy.data.images.load(path) 55 | return image 56 | 57 | def skip_job_if_file_exists(self, obj): 58 | if self.settings.use_overwrite: 59 | return False 60 | 61 | img_file_name = self.get_image_file_name(obj.name) 62 | tex_dir = Path(self.texture_folder) 63 | path = Path(bpy.path.abspath(self.settings.file_path)) / \ 64 | tex_dir / img_file_name 65 | 66 | if path.is_file(): 67 | self.report({'INFO'}, "baking skipped for '{0}'. File exists.".format( 68 | self.get_image_file_name(obj.name))) 69 | 70 | # load image for new material 71 | image = self.load_image_by(img_file_name) 72 | if not self.jobname in {'Color', 'Diffuse'}: 73 | image.colorspace_settings.name = 'Non-Color' 74 | self.new_images[self.jobname] = image 75 | 76 | return True 77 | return False 78 | 79 | def get_image_file_name(self, object_name): 80 | prefix = get_image_prefix(object_name) 81 | name = object_name if self.settings.use_object_name else "" 82 | suffix = get_image_suffix(self.jobname) 83 | if self.jobname == "Vertex Color": 84 | suffix += self.suffix_extension 85 | ending = IMAGE_FILE_FORMAT_ENDINGS[self.settings.file_format] 86 | img_name = f"{prefix}{name}{suffix}.{ending}" 87 | return img_name 88 | 89 | def get_image_file_path(self, image_file_name): 90 | img_file_name = remove_not_allowed_signs(image_file_name) 91 | tex_dir = Path(self.texture_folder) 92 | if self.settings.file_path.startswith("//"): 93 | rel_path = bpy.path.relpath(self.settings.file_path) 94 | path = rel_path + str(tex_dir / img_file_name) 95 | else: 96 | abs_path = bpy.path.abspath(self.settings.file_path) 97 | path = Path(abs_path) / tex_dir / img_file_name 98 | return str(path) 99 | 100 | def new_bake_image(self, object_name): 101 | img_name = self.get_image_file_name(object_name) 102 | path = self.get_image_file_path(img_name) 103 | 104 | # alpha 105 | alpha = False 106 | if self.settings.color_mode == 'RGBA' or (self.jobname == 'Color' and self.settings.use_alpha_to_color): 107 | alpha = True 108 | 109 | # color 110 | color = (0.0, 0.0, 0.0, 1.0) 111 | if get_bake_type_by(self.jobname) == 'NORMAL': 112 | color = (0.5, 0.5, 1.0, 1.0) 113 | 114 | # resolution 115 | res = int(self.settings.custom_resolution) if self.settings.resolution == 'CUSTOM' else int( 116 | self.settings.resolution) 117 | 118 | is_float = False if self.settings.color_depth == '8' else True 119 | 120 | image = bpy.data.images.new( 121 | name=img_name, width=res, height=res, alpha=alpha, float_buffer=is_float) 122 | 123 | if not self.jobname in {'Color', 'Diffuse'}: 124 | image.colorspace_settings.name = 'Non-Color' 125 | 126 | image.generated_color = color 127 | image.generated_type = 'BLANK' 128 | image.filepath = path 129 | 130 | return image 131 | 132 | def create_gloss_image(self, obj_name): 133 | if "Roughness" in self.new_images: 134 | self.jobname = "Glossiness" # for suffix 135 | gloss_image = self.new_bake_image(obj_name) 136 | gloss_img_name = self.get_image_file_name(obj_name) 137 | gloss_image.filepath = self.get_image_file_path(gloss_img_name) 138 | rough_img = self.new_images["Roughness"] 139 | gloss_image.pixels = get_invert_image(rough_img) 140 | gloss_image.save() 141 | save_image(gloss_image, self.get_color_mode(self.jobname)) 142 | gloss_image.reload() 143 | self.new_images[self.jobname] = gloss_image 144 | 145 | def check_texture_folder(self): 146 | abs_path = Path(bpy.path.abspath(self.settings.file_path)) 147 | path = str(abs_path / self.texture_folder) 148 | return check_path_access(path) 149 | 150 | def check_file_path(self): 151 | path = self.settings.file_path 152 | 153 | if path in {'', ' ', '/', '///', '\\', '//\\'}: 154 | self.report({'ERROR'}, f"'{path}' not a valid path") 155 | return False 156 | 157 | if check_path_access(path): 158 | return True 159 | else: 160 | self.report({'ERROR'}, f"No write permission to '{path}'!") 161 | 162 | def alpha_channel_to_color(self): 163 | """Add an alpha channel to the Color image in the list of newly created images.""" 164 | 165 | if "Color" in self.new_images.keys() and "Alpha" in self.new_images.keys(): 166 | img = get_combined_images( 167 | self.new_images["Color"], self.new_images["Alpha"], 0, 3) 168 | self.new_images["Color"].pixels = img 169 | self.new_images["Color"].save() 170 | 171 | def combine_channels(self, obj): 172 | """Combine all channels defined under 'Combined Channels'.""" 173 | 174 | if len(bpy.context.scene.principled_baker_combinelist) == 0: 175 | return 176 | 177 | for combi in bpy.context.scene.principled_baker_combinelist: 178 | if not combi.do_combine: 179 | continue 180 | 181 | # Color Mode 182 | if combi.channel_a in self.new_images: 183 | alpha = True 184 | color_mode = 'RGBA' 185 | else: 186 | alpha = False 187 | color_mode = 'RGB' 188 | 189 | # create new image 190 | object_name = obj.name 191 | prefix = get_image_prefix(object_name) 192 | name = object_name if self.settings.use_object_name else "" 193 | suffix = combi.suffix 194 | ending = IMAGE_FILE_FORMAT_ENDINGS[self.settings.file_format] 195 | img_name = f"{prefix}{name}{suffix}.{ending}" 196 | path = self.get_image_file_path(img_name) 197 | 198 | # resolution 199 | res = int(self.settings.custom_resolution) if self.settings.resolution == 'CUSTOM' else int( 200 | self.settings.resolution) 201 | 202 | is_float = False if self.settings.color_depth == '8' else True 203 | 204 | image = bpy.data.images.new( 205 | name=img_name, width=res, height=res, alpha=alpha, float_buffer=is_float) 206 | 207 | image.colorspace_settings.name = 'Non-Color' 208 | image.generated_color = (0, 0, 0, 1) 209 | image.generated_type = 'BLANK' 210 | image.filepath = path 211 | 212 | image.save() 213 | 214 | # combine 215 | r, g, b, a = None, None, None, None 216 | if combi.channel_r in self.new_images: 217 | r = self.new_images[combi.channel_r] 218 | if combi.channel_g in self.new_images: 219 | g = self.new_images[combi.channel_g] 220 | if combi.channel_b in self.new_images: 221 | b = self.new_images[combi.channel_b] 222 | if combi.channel_a in self.new_images: 223 | a = self.new_images[combi.channel_a] 224 | combine_channels_to_image( 225 | image, 226 | R=r, 227 | G=g, 228 | B=b, 229 | A=a, 230 | channel_r=int(combi.channel_r_from_channel), 231 | channel_g=int(combi.channel_g_from_channel), 232 | channel_b=int(combi.channel_b_from_channel), 233 | channel_a=int(combi.channel_a_from_channel), 234 | invert_r=combi.channel_r_invert, 235 | invert_g=combi.channel_g_invert, 236 | invert_b=combi.channel_b_invert, 237 | invert_a=combi.channel_a_invert, 238 | ) 239 | 240 | # Color Depth 241 | color_depth = '8' 242 | for img in {r, g, b, a}: 243 | if img: 244 | if img.is_float: 245 | if self.settings.file_format == 'OPEN_EXR': 246 | color_depth = '32' 247 | else: 248 | color_depth = '16' 249 | break 250 | 251 | # save image 252 | save_image_as(image, 253 | file_path=image.filepath, 254 | file_format=self.settings.file_format, 255 | color_mode=color_mode, 256 | color_depth=color_depth, 257 | compression=self.settings.compression, 258 | quality=self.settings.quality, 259 | tiff_codec=self.settings.tiff_codec, 260 | exr_codec=self.settings.exr_codec) 261 | image.reload() 262 | 263 | def bake(self, bake_type): 264 | """Wrapper for bpy.ops.object.bake() to get all parameters from settings""" 265 | 266 | pass_filter = [] 267 | if self.settings.use_Diffuse: 268 | if self.render_settings.use_pass_direct: 269 | pass_filter.append('DIRECT') 270 | if self.render_settings.use_pass_indirect: 271 | pass_filter.append('INDIRECT') 272 | if self.render_settings.use_pass_color: 273 | pass_filter.append('COLOR') 274 | pass_filter = set(pass_filter) 275 | 276 | selected_to_active = True if self.settings.bake_mode == 'SELECTED_TO_ACTIVE' else False 277 | 278 | bpy.ops.object.bake( 279 | type=bake_type, 280 | pass_filter=pass_filter, 281 | use_selected_to_active=selected_to_active, 282 | normal_space=self.render_settings.normal_space, 283 | normal_r=self.render_settings.normal_r, 284 | normal_g=self.render_settings.normal_g, 285 | normal_b=self.render_settings.normal_b, ) 286 | 287 | def bake_and_save(self, image, bake_type='EMIT'): 288 | """Bake and save image.""" 289 | 290 | image.save() 291 | self.report({'INFO'}, "baking '{0}'".format(image.name)) 292 | self.bake(bake_type) 293 | save_image(image, self.get_color_mode(self.jobname)) 294 | image.reload() 295 | 296 | def get_color_mode(self, jobname): 297 | if jobname == 'Color' and self.settings.use_alpha_to_color: 298 | return 'RGBA' 299 | else: 300 | return self.settings.color_mode 301 | 302 | # ------------------------------------------------------------------------- 303 | # CLEAN UPS! 304 | # ------------------------------------------------------------------------- 305 | def final_cleanup(self): 306 | """Restore temporarily changed settings.""" 307 | 308 | # Auto Smooth - Clean up! 309 | if not self.settings.auto_smooth == 'OBJECT': 310 | for obj in self.auto_smooth_list: 311 | obj.data.use_auto_smooth = self.auto_smooth_list[obj] 312 | 313 | # Render Engine - Clean up! 314 | if self.prefs.switch_to_cycles: 315 | bpy.context.scene.render.engine = self.render_engine 316 | bpy.context.scene.cycles.preview_pause = self.preview_pause 317 | 318 | # Samples 319 | bpy.context.scene.cycles.samples = self.org_samples 320 | 321 | # Restore orig selection 322 | for obj in bpy.context.selected_objects: 323 | obj.select_set(False) 324 | for obj in self.selected_objects: 325 | obj.select_set(True) 326 | bpy.context.view_layer.objects.active = self.active_object 327 | 328 | def clean_after_bake(self, objects): 329 | for obj in objects: 330 | # delete temp materials 331 | delete_tagged_materials(obj, MATERIAL_TAG_VERTEX) 332 | 333 | # delete temp nodes 334 | delete_tagged_nodes_in_object(obj) 335 | 336 | # reselect UV Map 337 | if not self.settings.set_selected_uv_map: 338 | if obj in self.orig_uv_layers_active_indices: 339 | uv_layers = obj.data.uv_layers 340 | uv_layers.active_index = self.orig_uv_layers_active_indices[obj] 341 | 342 | # deactivate all Material Outputs before reactivating 343 | for mat_slot in obj.material_slots: 344 | if mat_slot.material: 345 | deactivate_material_outputs(mat_slot.material) 346 | 347 | # reactivate Material Outputs 348 | for mat_output in self.active_outputs: 349 | mat_output.is_active_output = True 350 | 351 | # ------------------------------------------------------------------------- 352 | # TEST CONDITIONS 353 | # ------------------------------------------------------------------------- 354 | def can_execute(self, context): 355 | """Test necessary conditions and report errors. 356 | 357 | :returns: True, if baking is possible, else False. 358 | """ 359 | 360 | # Bake only works in cycles (for now) 361 | if not bpy.context.scene.render.engine == 'CYCLES' and not self.prefs.switch_to_cycles: 362 | self.report({'ERROR'}, 'Error: Current render engine ({0}) does not support baking'.format( 363 | bpy.context.scene.render.engine)) 364 | return False 365 | 366 | if not context.selected_objects: 367 | self.report({'INFO'}, "Nothing selected.") 368 | return False 369 | 370 | # File needs to be saved 371 | if not bpy.data.is_saved: 372 | self.report( 373 | {'ERROR'}, 'Blendfile needs to be saved to get relative output paths') 374 | return False 375 | 376 | # Check file path 377 | if not self.check_file_path(): 378 | return False 379 | 380 | if self.settings.bake_mode == 'SELECTED_TO_ACTIVE': 381 | if len(context.selected_objects) < 2: 382 | self.report({'ERROR'}, 'Select at least 2 objects!') 383 | return False 384 | 385 | if self.settings.use_Diffuse: 386 | d = self.render_settings.use_pass_direct 387 | i = self.render_settings.use_pass_indirect 388 | c = self.render_settings.use_pass_color 389 | if not d and not i and not c: 390 | self.report( 391 | {'ERROR'}, "Error: Bake pass requires Direct, Indirect, or Color contributions to be enabled.") 392 | return False 393 | 394 | # all fine 395 | return True 396 | 397 | def can_bake(self, objects): 398 | """Test conditions and report errors. 399 | 400 | Objects must: 401 | not be hidden, 402 | have a material (expect baking vertex color), 403 | have no empty material slots. 404 | 405 | :returns: True, if baking is possible, else False. 406 | """ 407 | 408 | for obj in objects: 409 | # enabled for rendering? 410 | if obj.hide_render: 411 | self.report( 412 | {'INFO'}, "baking cancelled. '{0}' not enabled for rendering.".format(obj.name)) 413 | return False 414 | # no material or missing output? 415 | if not has_material(obj): 416 | if not self.settings.use_vertex_color: 417 | self.report( 418 | {'INFO'}, "baking cancelled. '{0}' Material missing, Material Output missing, or Material Output input missing.".format(obj.name)) 419 | return False 420 | 421 | # empty material slots? 422 | for mat_slot in obj.material_slots: 423 | if not mat_slot.material: 424 | self.report( 425 | {'INFO'}, "baking cancelled. '{0}' has empty Material Slots.".format(obj.name)) 426 | return False 427 | 428 | # has every object a UV map? 429 | if self.settings.auto_uv_project == 'OFF': 430 | objs_with_missing_uv_map = [] 431 | for obj in objects: 432 | if len(obj.data.uv_layers) == 0: 433 | objs_with_missing_uv_map.append(obj.name) 434 | if len(objs_with_missing_uv_map) > 0: 435 | self.report({'ERROR'}, 436 | "UV map missing: '{0}'".format(objs_with_missing_uv_map)) 437 | return False 438 | 439 | return True 440 | 441 | def is_joblist_empty(self): 442 | if not self.joblist: 443 | self.final_cleanup() 444 | self.report({'INFO'}, "Nothing to do.") 445 | return True 446 | 447 | # ------------------------------------------------------------------------- 448 | # PREPARATIONS 449 | # ------------------------------------------------------------------------- 450 | def prepare_materials_of_objects_by_jobname(self, objects, jobname, vertex_color_name=""): 451 | """Prepares the all materials for all bake objects for baking. 452 | 453 | Temporay nodes must be removed afer baking! 454 | """ 455 | 456 | if jobname == 'Material ID': 457 | prepare_objects_for_bake_matid(objects) 458 | elif jobname == 'Vertex Color': 459 | prepare_objects_for_bake_vertex_color(objects, vertex_color_name) 460 | elif jobname == 'Wireframe': 461 | prepare_objects_for_bake_wireframe(objects) 462 | elif jobname == 'Diffuse': 463 | pass # prepare nothing 464 | else: 465 | prepare_objects_for_bake(objects, jobname) 466 | 467 | # ------------------------------------------------------------------------- 468 | # BAKE COMBINED: 469 | # ------------------------------------------------------------------------- 470 | def bake_combined(self, context, bake_objects): 471 | if not self.can_bake(bake_objects): 472 | self.final_cleanup() 473 | return {'CANCELLED'} 474 | 475 | # Populate joblist 476 | self.joblist = get_joblist_from_objects(bake_objects) 477 | 478 | # skip, if one object has no vertex color 479 | if self.settings.use_vertex_color: 480 | objects_without_vertex_color = [] 481 | for obj in bake_objects: 482 | if len(obj.data.vertex_colors) == 0: 483 | objects_without_vertex_color.append(obj.name) 484 | if len(objects_without_vertex_color) > 0: 485 | self.report( 486 | {'INFO'}, f"Objects have no Vertex Color: '{objects_without_vertex_color}'") 487 | return {'CANCELLED'} 488 | 489 | # empty joblist -> nothing to do 490 | if self.is_joblist_empty(): 491 | return {'CANCELLED'} 492 | 493 | # material outpus for later clean up 494 | self.active_outputs = set(get_active_outputs(bake_objects)) 495 | self.all_material_outputs = get_all_material_outputs(bake_objects) 496 | set_material_outputs_target_to_all(bake_objects) 497 | 498 | # Auto UV project 499 | auto_uv_project(bake_objects) 500 | 501 | # UV Map selection 502 | for obj in bake_objects: 503 | self.orig_uv_layers_active_indices[obj] = obj.data.uv_layers.active_index 504 | select_uv_map(obj) 505 | 506 | active_object = self.active_object 507 | if self.settings.bake_mode == "BATCH": 508 | active_object = bake_objects[0] 509 | 510 | # (optional) new material 511 | self.new_pri_node_values = get_principled_node_values(bake_objects) 512 | 513 | new_mat_name = self.settings.new_material_prefix 514 | if self.settings.make_new_material or self.settings.duplicate_objects: 515 | if self.settings.new_material_prefix == "": 516 | if self.settings.bake_mode == "BATCH": 517 | new_mat_name = bake_objects[0].name 518 | else: 519 | new_mat_name = active_object.name 520 | new_mat = new_material(new_mat_name, self.new_pri_node_values) 521 | 522 | # texture folder 523 | if self.settings.use_texture_folder: 524 | if self.settings.bake_mode == "BATCH": 525 | tex_dir = bake_objects[0].name 526 | else: 527 | tex_dir = active_object.name 528 | self.texture_folder = remove_not_allowed_signs(tex_dir) 529 | 530 | if not self.check_texture_folder(): 531 | self.report({'ERROR'}, 'Error: Texture Folder') 532 | return {'CANCELLED'} 533 | 534 | # Go through joblist 535 | for self.jobname in self.joblist: 536 | 537 | # [""] <- empty string as the one object to iterate over for non-vertex-colors-jobs 538 | subjobs = [""] 539 | if self.jobname == "Vertex Color": 540 | subjobs = get_vertex_colors_to_bake_from_objects(bake_objects) 541 | 542 | for subname in subjobs: 543 | self.suffix_extension = subname 544 | 545 | # skip job, if no overwrite and image exists. load existing image 546 | if self.skip_job_if_file_exists(active_object): 547 | continue 548 | 549 | # set individual samples 550 | set_samples(self.jobname) 551 | 552 | # Prepare materials 553 | self.prepare_materials_of_objects_by_jobname( 554 | bake_objects, self.jobname, subname) 555 | 556 | # image to bake on 557 | image = self.new_bake_image(active_object.name) 558 | 559 | # append image to image dict for new material 560 | self.new_images[self.jobname + self.suffix_extension] = image 561 | 562 | # image nodes to bake 563 | create_bake_image_nodes(bake_objects, image) 564 | 565 | # Bake and Save image! 566 | self.bake_and_save( 567 | image, 568 | bake_type=get_bake_type_by(self.jobname)) 569 | 570 | self.clean_after_bake(bake_objects) 571 | 572 | # jobs DONE 573 | 574 | # Glossiness 575 | if self.settings.use_invert_roughness: 576 | self.create_gloss_image(obj.name) 577 | 578 | # Add alpha channel to color 579 | if self.settings.use_alpha_to_color: 580 | self.alpha_channel_to_color() 581 | 582 | # Duplicate objects 583 | if self.settings.duplicate_objects: 584 | if self.settings.bake_mode == "BATCH": 585 | self.dup_objects.append( 586 | duplicate_object(bake_objects[0], new_mat)) 587 | else: 588 | self.dup_objects.extend(duplicate_objects( 589 | self.active_object, bake_objects, new_mat)) 590 | 591 | # add new images to new material 592 | if self.settings.make_new_material or self.settings.duplicate_objects: 593 | add_images_to_material(self.new_images, new_mat) 594 | self.report( 595 | {'INFO'}, "Mew Material created. '{0}'".format(new_mat.name)) 596 | 597 | # (optional) add new material 598 | if self.settings.add_new_material: 599 | active_object.data.materials.append(new_mat) 600 | 601 | # Clean up! 602 | for mat_output, target in self.all_material_outputs.items(): 603 | mat_output.target = target 604 | 605 | # remove tag from new material 606 | if self.settings.make_new_material or self.settings.duplicate_objects: 607 | if MATERIAL_TAG in new_mat: 608 | del(new_mat[MATERIAL_TAG]) 609 | 610 | # Combine channels 611 | self.combine_channels(active_object) 612 | 613 | # ------------------------------------------------------------------------- 614 | # BAKE BATCH: 615 | # ------------------------------------------------------------------------- 616 | def bake_batch(self, context, bake_objects): 617 | if not self.can_bake(bake_objects): 618 | self.final_cleanup() 619 | return {'CANCELLED'} 620 | 621 | # Deselect all 622 | context.view_layer.objects.active = None 623 | for obj in bake_objects: 624 | obj.select_set(False) 625 | 626 | for obj in bake_objects: 627 | self.new_images.clear() 628 | 629 | obj.select_set(True) 630 | context.view_layer.objects.active = obj 631 | self.bake_combined(context, [obj]) 632 | obj.select_set(False) 633 | 634 | # ------------------------------------------------------------------------- 635 | # BAKE SELECTED TO ACTIVE: 636 | # ------------------------------------------------------------------------- 637 | def bake_selected_to_active(self, context, bake_objects): 638 | active_object = self.active_object 639 | 640 | # exclude active object from selected objects 641 | if self.settings.bake_mode == 'SELECTED_TO_ACTIVE': 642 | if active_object in bake_objects: 643 | bake_objects.remove(active_object) 644 | 645 | if not self.can_bake(bake_objects): 646 | self.final_cleanup() 647 | return {'CANCELLED'} 648 | 649 | for mat_slot in active_object.material_slots: 650 | if not mat_slot.material: 651 | self.report({'INFO'}, "baking cancelled. '{0}' has empty Material Slots.".format( 652 | active_object.name)) 653 | return False 654 | 655 | # has active object UV map? 656 | if self.settings.auto_uv_project == 'OFF': 657 | if len(active_object.data.uv_layers) == 0: 658 | self.report( 659 | {'INFO'}, "baking cancelled. '{0}' UV map missing.".format(active_object.name)) 660 | self.final_cleanup() 661 | return {'CANCELLED'} 662 | 663 | # Can bake? 664 | for obj in bake_objects: 665 | # enabled for rendering? 666 | if obj.hide_render: 667 | self.report( 668 | {'INFO'}, "baking cancelled. '{0}' not enabled for rendering.".format(obj.name)) 669 | self.final_cleanup() 670 | return {'CANCELLED'} 671 | 672 | # Populate joblist 673 | self.joblist = get_joblist_from_objects(bake_objects) 674 | 675 | # empty joblist -> nothing to do 676 | if self.is_joblist_empty(): 677 | return {'CANCELLED'} 678 | 679 | # material outpus for later clean up 680 | self.active_outputs = set(get_active_outputs(bake_objects)) 681 | self.all_material_outputs = get_all_material_outputs(bake_objects) 682 | set_material_outputs_target_to_all(bake_objects) 683 | 684 | # Auto UV project 685 | if not self.settings.auto_uv_project == 'OFF': 686 | auto_uv_project(active_object) 687 | 688 | # UV Map selection 689 | self.orig_uv_layers_active_indices[active_object] = active_object.data.uv_layers.active_index 690 | select_uv_map(active_object) 691 | 692 | # new material 693 | self.new_pri_node_values = get_principled_node_values(bake_objects) 694 | new_mat_name = self.settings.new_material_prefix 695 | if self.settings.new_material_prefix == "": 696 | new_mat_name = active_object.name 697 | new_mat = new_material(new_mat_name, self.new_pri_node_values) 698 | active_object.data.materials.append(new_mat) 699 | 700 | # texture folder 701 | if self.settings.use_texture_folder: 702 | tex_dir = active_object.name 703 | self.texture_folder = remove_not_allowed_signs(tex_dir) 704 | 705 | # Go through joblist 706 | for self.jobname in self.joblist: 707 | 708 | # skip, if job is not "Vertex Color" and has no material 709 | if not self.jobname == "Vertex Color": 710 | skip = any(not has_material(obj) for obj in bake_objects) 711 | if skip: 712 | self.report( 713 | {'INFO'}, "{0} baking skipped for '{1}'".format(self.jobname, obj.name)) 714 | continue 715 | 716 | # [""] <- empty string as the one object to iterate over for non-vertex-colors-jobs 717 | subjobs = [""] 718 | if self.jobname == "Vertex Color": 719 | subjobs = get_vertex_colors_to_bake_from_objects(bake_objects) 720 | 721 | for subname in subjobs: 722 | self.suffix_extension = subname 723 | 724 | # skip job, if no overwrite and image exists. load existing image 725 | if self.skip_job_if_file_exists(active_object): 726 | continue 727 | 728 | # set individual samples 729 | set_samples(self.jobname) 730 | 731 | # temp material for vertex color or wireframe 732 | if self.settings.use_vertex_color or self.settings.use_wireframe: 733 | for obj in bake_objects: 734 | if not has_material(obj): 735 | add_temp_material(obj) 736 | 737 | # Prepare materials 738 | self.prepare_materials_of_objects_by_jobname( 739 | bake_objects, self.jobname, subname) 740 | 741 | # image to bake on 742 | image = self.new_bake_image(active_object.name) 743 | 744 | # append image to image dict for new material 745 | self.new_images[self.jobname + self.suffix_extension] = image 746 | 747 | # image nodes to bake 748 | create_bake_image_nodes([active_object], image) 749 | 750 | # Bake and Save image! 751 | self.bake_and_save( 752 | image, 753 | bake_type=get_bake_type_by(self.jobname)) 754 | 755 | self.clean_after_bake(bake_objects) 756 | self.clean_after_bake([active_object]) 757 | 758 | # jobs DONE 759 | 760 | # Glossiness 761 | if self.settings.use_invert_roughness: 762 | self.create_gloss_image(obj.name) 763 | 764 | # Add alpha channel to color 765 | if self.settings.use_alpha_to_color: 766 | self.alpha_channel_to_color() 767 | 768 | # add new images to new material 769 | if self.settings.make_new_material or self.settings.duplicate_objects: 770 | add_images_to_material(self.new_images, new_mat) 771 | self.report( 772 | {'INFO'}, "Mew Material created. '{0}'".format(new_mat.name)) 773 | 774 | # (optional) add new material 775 | if self.settings.add_new_material: 776 | active_object.data.materials.append(new_mat) 777 | 778 | # Clean up! 779 | for mat_output, target in self.all_material_outputs.items(): 780 | mat_output.target = target 781 | 782 | # remove tag from new material 783 | if self.settings.make_new_material or self.settings.duplicate_objects: 784 | if MATERIAL_TAG in new_mat: 785 | del(new_mat[MATERIAL_TAG]) 786 | 787 | # Combine channels 788 | self.combine_channels(active_object) 789 | 790 | # ------------------------------------------------------------------------- 791 | # EXECUTE 792 | # ------------------------------------------------------------------------- 793 | def execute(self, context): 794 | 795 | self.prefs = context.preferences.addons[__package__].preferences 796 | 797 | self.settings = context.scene.principled_baker_settings 798 | self.render_settings = context.scene.render.bake 799 | 800 | if not self.can_execute(context): 801 | return {'CANCELLED'} 802 | 803 | self.active_object = context.active_object 804 | if not self.active_object: 805 | context.view_layer.objects.active = context.selected_objects[0] 806 | self.active_object = context.active_object 807 | if not self.active_object.type == 'MESH': 808 | self.report({'ERROR'}, '{0} is not a mesh object'.format( 809 | self.active_object.name)) 810 | return {'CANCELLED'} 811 | 812 | self.selected_objects = context.selected_objects 813 | for obj in self.selected_objects: 814 | if not obj.type == 'MESH': 815 | obj.select_set(False) 816 | 817 | self.bake_objects = [] 818 | 819 | # active object is first item in bake_objects 820 | self.bake_objects.append(self.active_object) 821 | self.bake_objects.extend(get_only_meshes(self.selected_objects)) 822 | self.bake_objects = list(set(self.bake_objects)) 823 | 824 | self.dup_objects = [] 825 | 826 | self.texture_folder = "" 827 | 828 | # Temp switch to Cycles - see clean up! 829 | self.render_engine = context.scene.render.engine 830 | self.preview_pause = context.scene.cycles.preview_pause 831 | if not self.render_engine == 'CYCLES' and self.prefs.switch_to_cycles: 832 | context.scene.cycles.preview_pause = True 833 | context.scene.render.engine = 'CYCLES' 834 | 835 | # Init Suffix List, if not existing 836 | if not len(context.scene.principled_baker_suffixlist): 837 | bpy.ops.principled_baker_suffixlist.init() 838 | 839 | # Auto Smooth - See clean up! 840 | self.auto_smooth_list = {} 841 | if not self.settings.auto_smooth == 'OBJECT': 842 | for obj in self.bake_objects: 843 | self.auto_smooth_list[obj] = obj.data.use_auto_smooth 844 | if self.settings.auto_smooth == 'ON': 845 | for obj in self.bake_objects: 846 | obj.data.use_auto_smooth = True 847 | elif self.settings.auto_smooth == 'OFF': 848 | for obj in self.bake_objects: 849 | obj.data.use_auto_smooth = False 850 | 851 | # Samples to restore - See clean up! 852 | self.org_samples = context.scene.cycles.samples 853 | 854 | # images for new material. "name":image 855 | self.new_images = {} 856 | 857 | self.joblist = [] 858 | 859 | self.jobname = "" # current bake job 860 | 861 | # current suffix extension used for vertex colors only 862 | self.suffix_extension = "" 863 | 864 | # store equal node values for the Principled BSDF node in new material 865 | # TODO option to copy principled node values 866 | self.new_pri_node_values = {} 867 | 868 | self.orig_uv_layers_active_indices = {} 869 | self.active_outputs = [] 870 | 871 | # deligate bake modes 872 | if self.settings.bake_mode == 'COMBINED': 873 | self.bake_combined(context, self.bake_objects) 874 | elif self.settings.bake_mode == 'BATCH': 875 | self.bake_batch(context, self.bake_objects) 876 | elif self.settings.bake_mode == 'SELECTED_TO_ACTIVE': 877 | self.bake_selected_to_active(context, self.bake_objects) 878 | 879 | # join duplicate objects 880 | if self.settings.join_duplicate_objects and len(self.dup_objects) > 1: 881 | for obj in self.dup_objects: 882 | obj.select_set(True) 883 | bpy.ops.object.join() 884 | 885 | # copy modifiers from active object 886 | if self.settings.copy_modifiers: 887 | dup_obj = bpy.context.view_layer.objects.active 888 | dup_obj.modifiers.clear() 889 | bpy.context.view_layer.objects.active = self.active_object 890 | bpy.ops.object.make_links_data(type='MODIFIERS') 891 | 892 | self.final_cleanup() 893 | 894 | return {'FINISHED'} 895 | -------------------------------------------------------------------------------- /check_path_access.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import bpy 5 | 6 | 7 | def check_path_access(path): 8 | if path.startswith("//"): 9 | abs_path = Path(bpy.path.abspath(path)) 10 | else: 11 | abs_path = Path(path) 12 | 13 | write_permission = False 14 | try: 15 | abs_path.mkdir(parents=True, exist_ok=True) 16 | except PermissionError: 17 | write_permission = False 18 | 19 | if os.access(path=abs_path, mode=os.W_OK): 20 | write_permission = True 21 | 22 | return write_permission 23 | -------------------------------------------------------------------------------- /combinelist.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty, EnumProperty, StringProperty 3 | from bpy.types import Operator, PropertyGroup, UIList 4 | 5 | 6 | def channel_items(scene, context): 7 | items = [ 8 | ('None', "None", ""), 9 | ('Color', "Color", ""), 10 | ('Metallic', "Metallic", ""), 11 | ('Specular', "Specular", ""), 12 | ('Roughness', "Roughness", ""), 13 | ('Glossiness', "Glossiness", ""), 14 | ('Ambient Occlusion', "Ambient Occlusion", ""), 15 | ('Alpha', "Alpha", ""), 16 | ('Bump', "Bump", ""), 17 | ('Displacement', "Displacement", ""), 18 | ('Normal', "Normal", ""), 19 | ('Emission', "Emission", ""), 20 | ('Subsurface', "Subsurface", ""), 21 | ('Subsurface Radius', "Subsurface Radius", ""), 22 | ('Subsurface Color', "Subsurface Color", ""), 23 | ('Specular Tint', "Specular Tint", ""), 24 | ('Anisotropic', "Anisotropic", ""), 25 | ('Anisotropic Rotation', "Anisotropic Rotation", ""), 26 | ('Sheen', "Sheen", ""), 27 | ('Sheen Tint', "Sheen Tint", ""), 28 | ('Clearcoat', "Clearcoat", ""), 29 | ('Clearcoat Roughness', "Clearcoat Roughness", ""), 30 | ('IOR', "IOR", ""), 31 | ('Transmission', "Transmission", ""), 32 | ('Transmission Roughness', "Transmission Roughness", ""), 33 | ('Clearcoat Normal', "Clearcoat Normal", ""), 34 | ('Tangent', "Tangent", ""), 35 | ] 36 | return items 37 | 38 | 39 | def from_channel_items(scene, context): 40 | return [ 41 | ('0', "R", ""), 42 | ('1', "G", ""), 43 | ('2', "B", ""), 44 | ('3', "A", ""), 45 | ] 46 | 47 | 48 | class PBAKER_CombineListItem(PropertyGroup): 49 | suffix: StringProperty(name="Suffix", default="_combined") 50 | 51 | do_combine: BoolProperty(name="", default=True) 52 | 53 | channel_r: EnumProperty(name="R", items=channel_items, 54 | description="Red Channel") 55 | channel_g: EnumProperty(name="G", items=channel_items, 56 | description="Green Channel") 57 | channel_b: EnumProperty(name="B", items=channel_items, 58 | description="Blue Channel") 59 | channel_a: EnumProperty(name="A", items=channel_items, 60 | description="Alpha Channel") 61 | 62 | channel_r_from_channel: EnumProperty( 63 | name="channel", items=from_channel_items, description="from channel") 64 | channel_g_from_channel: EnumProperty( 65 | name="channel", items=from_channel_items, description="from channel") 66 | channel_b_from_channel: EnumProperty( 67 | name="channel", items=from_channel_items, description="from channel") 68 | channel_a_from_channel: EnumProperty( 69 | name="channel", items=from_channel_items, description="from channel") 70 | 71 | channel_r_invert: BoolProperty(name="R invert", description="Red invert") 72 | channel_g_invert: BoolProperty(name="G invert", description="Green invert") 73 | channel_b_invert: BoolProperty(name="B invert", description="Blue invert") 74 | channel_a_invert: BoolProperty(name="A invert", description="Alpha invert") 75 | 76 | 77 | class PBAKER_UL_CombineList(UIList): 78 | def draw_item(self, context, layout, data, item, icon, active_data, 79 | active_propname, index): 80 | 81 | if self.layout_type == 'DEFAULT': 82 | layout.prop(item, 'do_combine', text='') 83 | layout.prop(item, 'name', text='', emboss=False, translate=False) 84 | layout.prop(item, 'suffix', text='') 85 | elif self.layout_type == 'COMPACT': 86 | col = layout.column() 87 | col.label(text=item.name) 88 | col2 = col.column() 89 | col2.prop(item, "suffix", text="Suffix") 90 | row = col2.row() 91 | row.prop(item, "channel_r") 92 | row.prop(item, "channel_r_from_channel", text='') 93 | row.prop(item, "channel_r_invert", text='invert') 94 | row = col2.row() 95 | row.prop(item, "channel_g") 96 | row.prop(item, "channel_g_from_channel", text='') 97 | row.prop(item, "channel_g_invert", text='invert') 98 | row = col2.row() 99 | row.prop(item, "channel_b") 100 | row.prop(item, "channel_b_from_channel", text='') 101 | row.prop(item, "channel_b_invert", text='invert') 102 | row = col2.row() 103 | row.prop(item, "channel_a") 104 | row.prop(item, "channel_a_from_channel", text='') 105 | row.prop(item, "channel_a_invert", text='invert') 106 | row = col2.row() 107 | 108 | 109 | class PBAKER_COMBINELIST_OT_Add(Operator): 110 | """Create Combine List to customize combinees""" 111 | 112 | bl_idname = "principled_baker_combinelist.add" 113 | bl_label = "Init Combinelist" 114 | 115 | def execute(self, context): 116 | combinelist = bpy.context.scene.principled_baker_combinelist 117 | item = combinelist.add() 118 | item.name = "Combine" 119 | 120 | return{'FINISHED'} 121 | 122 | 123 | class PBAKER_COMBINELIST_OT_Delete(Operator): 124 | bl_idname = "principled_baker_combinelist.delete" 125 | bl_label = "Delete Combinelist" 126 | 127 | @classmethod 128 | def poll(cls, context): 129 | return context.scene.principled_baker_combinelist 130 | 131 | def execute(self, context): 132 | combinelist = context.scene.principled_baker_combinelist 133 | index = context.scene.principled_baker_combinelist_index 134 | combinelist.remove(index) 135 | context.scene.principled_baker_combinelist_index = min( 136 | max(0, index - 1), len(combinelist) - 1) 137 | 138 | return{'FINISHED'} 139 | 140 | 141 | class PBAKER_COMBINELIST_OT_MoveItem_Up(Operator): 142 | bl_idname = "principled_baker_combinelist.move_up" 143 | bl_label = "Up" 144 | 145 | @classmethod 146 | def poll(cls, context): 147 | return context.scene.principled_baker_combinelist 148 | 149 | def execute(self, context): 150 | combinelist = context.scene.principled_baker_combinelist 151 | index = bpy.context.scene.principled_baker_combinelist_index 152 | prev_index = index - 1 153 | combinelist.move(prev_index, index) 154 | new_index = index - 1 155 | list_len = len(combinelist) 156 | context.scene.principled_baker_combinelist_index = max( 157 | 0, min(new_index, list_len - 1)) 158 | 159 | return{'FINISHED'} 160 | 161 | 162 | class PBAKER_COMBINELIST_OT_MoveItem_Down(Operator): 163 | bl_idname = "principled_baker_combinelist.move_down" 164 | bl_label = "Down" 165 | 166 | @classmethod 167 | def poll(cls, context): 168 | return context.scene.principled_baker_combinelist 169 | 170 | def execute(self, context): 171 | combinelist = context.scene.principled_baker_combinelist 172 | index = bpy.context.scene.principled_baker_combinelist_index 173 | next_index = index + 1 174 | combinelist.move(next_index, index) 175 | new_index = index + 1 176 | list_len = len(combinelist) 177 | context.scene.principled_baker_combinelist_index = max( 178 | 0, min(new_index, list_len - 1)) 179 | 180 | return{'FINISHED'} 181 | -------------------------------------------------------------------------------- /const.py: -------------------------------------------------------------------------------- 1 | PB_PACKAGE = __package__ 2 | 3 | NODE_TAG = 'p_baker_node' 4 | MATERIAL_TAG = 'p_baker_material' 5 | MATERIAL_TAG_VERTEX = 'p_baker_material_vertex' 6 | 7 | NODE_INPUTS = [ 8 | 'Color', 9 | 'Subsurface', 10 | # 'Subsurface Radius', # TODO? 11 | 'Subsurface Color', 12 | 'Metallic', 13 | 'Specular', 14 | 'Specular Tint', 15 | 'Roughness', 16 | 'Anisotropic', 17 | 'Anisotropic Rotation', 18 | 'Sheen', 19 | 'Sheen Tint', 20 | 'Clearcoat', 21 | 'Clearcoat Roughness', 22 | 'IOR', 23 | 'Transmission', 24 | 'Transmission Roughness', 25 | 'Emission', 26 | 'Alpha', 27 | 'Normal', 28 | 'Clearcoat Normal', 29 | 'Tangent' 30 | ] 31 | 32 | # for new material to have images nicely sorted 33 | NODE_INPUTS_SORTED = [ 34 | 'Color', 35 | 'Ambient Occlusion', 36 | 'Subsurface', 37 | 'Subsurface Radius', 38 | 'Subsurface Color', 39 | 'Metallic', 40 | 'Specular', 41 | 'Specular Tint', 42 | 'Roughness', 43 | 'Glossiness', 44 | 'Anisotropic', 45 | 'Anisotropic Rotation', 46 | 'Sheen', 47 | 'Sheen Tint', 48 | 'Clearcoat', 49 | 'Clearcoat Roughness', 50 | 'IOR', 51 | 'Transmission', 52 | 'Transmission Roughness', 53 | 'Emission', 54 | 'Alpha', 55 | 'Normal', 56 | 'Clearcoat Normal', 57 | 'Tangent', 58 | 'Bump', 59 | 'Displacement', 60 | 'Diffuse', 61 | 'Wireframe', 62 | 'Material ID' 63 | ] 64 | 65 | NORMAL_INPUTS = {'Normal', 'Clearcoat Normal', 'Tangent'} 66 | 67 | ALPHA_NODES = { 68 | # "Alpha":'BSDF_TRANSPARENT', 69 | "Translucent_Alpha": 'BSDF_TRANSLUCENT', 70 | "Glass_Alpha": 'BSDF_GLASS' 71 | } 72 | 73 | BSDF_NODES = { 74 | 'BSDF_PRINCIPLED', 75 | 'BSDF_DIFFUSE', 76 | 'BSDF_TOON', 77 | 'BSDF_VELVET', 78 | 'BSDF_GLOSSY', 79 | 'BSDF_TRANSPARENT', 80 | 'BSDF_TRANSLUCENT', 81 | 'BSDF_GLASS' 82 | } 83 | 84 | IMAGE_FILE_FORMAT_ENDINGS = { 85 | "BMP": "bmp", 86 | "PNG": "png", 87 | "JPEG": "jpg", 88 | "TIFF": "tif", 89 | "TARGA": "tga", 90 | "OPEN_EXR": "exr", 91 | } 92 | 93 | # signs not allowed in file names or paths 94 | NOT_ALLOWED_SIGNS = ['\\', '/', ':', '*', '?', '"', '<', '>', '|'] 95 | -------------------------------------------------------------------------------- /duplicate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def duplicate_object(obj, new_mat): 5 | """Duplicate object and append new material. 6 | 7 | :returns: Object reference to new object. 8 | """ 9 | 10 | settings = bpy.context.scene.principled_baker_settings 11 | 12 | dup_obj = None 13 | # Duplicate object 14 | for o in bpy.context.selected_objects: 15 | o.select_set(False) 16 | bpy.context.view_layer.objects.active = obj 17 | obj.select_set(True) 18 | bpy.ops.object.duplicate() 19 | dup_obj = bpy.context.active_object 20 | 21 | # Rename 22 | prefix = settings.duplicate_objects_prefix 23 | suffix = settings.duplicate_objects_suffix 24 | if prefix or suffix: 25 | dup_obj.name = prefix + dup_obj.name[:-4] + suffix 26 | 27 | # Relocate duplicat object 28 | dup_obj.location.x += settings.duplicate_object_loc_offset_x 29 | dup_obj.location.y += settings.duplicate_object_loc_offset_y 30 | dup_obj.location.z += settings.duplicate_object_loc_offset_z 31 | 32 | # Remove all but selected UV Map 33 | uv_layers = dup_obj.data.uv_layers 34 | active_uv_layer_name = dup_obj.data.uv_layers.active.name 35 | 36 | uv_layers_to_delete = [] 37 | for uv_layer in uv_layers: 38 | if not uv_layer.name == active_uv_layer_name: 39 | uv_layers_to_delete.append(uv_layer.name) 40 | for uv_layer_name in uv_layers_to_delete: 41 | uv_layers.remove(uv_layers[uv_layer_name]) 42 | 43 | # Remove all materials 44 | for i in range(0, len(dup_obj.material_slots)): 45 | bpy.context.object.active_material_index = i 46 | bpy.ops.object.material_slot_remove({'object': dup_obj}) 47 | 48 | # Remove all modifiers 49 | if not settings.copy_modifiers: 50 | dup_obj.modifiers.clear() 51 | 52 | # Add new material 53 | dup_obj.data.materials.append(new_mat) 54 | 55 | dup_obj.select_set(False) 56 | 57 | # dup_objects.append(dup_obj) 58 | 59 | return dup_obj 60 | 61 | 62 | def duplicate_objects(active_object, objs, new_mat): 63 | """Duplicate a list of objecs and append new material to each. 64 | 65 | :returns: List of Object references to new objects. 66 | """ 67 | 68 | active_dup_obj = duplicate_object(active_object, new_mat) 69 | 70 | objs = objs.copy() 71 | if active_object in objs: 72 | objs.remove(active_object) 73 | 74 | dup_objects = [] 75 | 76 | for obj in objs: 77 | dup_obj = duplicate_object(obj, new_mat) 78 | uv_layers = dup_obj.data.uv_layers 79 | 80 | # Equal UV Map names 81 | uv_layers[0].name = active_dup_obj.data.uv_layers.active.name 82 | 83 | dup_objects.append(dup_obj) 84 | 85 | return dup_objects 86 | -------------------------------------------------------------------------------- /functions.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .const import NORMAL_INPUTS, NOT_ALLOWED_SIGNS 4 | from .nodes.find import find_node_by_type 5 | 6 | 7 | def is_list_equal(list): 8 | """:returns: True, if all items in list are equal""" 9 | 10 | if not list: 11 | return False # False, if list is empty 12 | first = list[0] 13 | return all(first == item for item in list[1:]) 14 | 15 | 16 | def get_only_meshes(objects) -> list: 17 | l = [] 18 | for o in objects: 19 | if o.type == 'MESH': 20 | l.append(o) 21 | return l 22 | 23 | 24 | def get_bake_type_by(jobname) -> str: 25 | if jobname in NORMAL_INPUTS: 26 | return 'NORMAL' 27 | if jobname in {'Diffuse'}: 28 | return 'DIFFUSE' 29 | else: 30 | return 'EMIT' 31 | 32 | 33 | def remove_not_allowed_signs(string) -> str: 34 | """ 35 | :returns: String (eg. from object name) 36 | without all signs not allowed in file names and paths. 37 | """ 38 | 39 | for s in NOT_ALLOWED_SIGNS: 40 | string = string.replace(s, "") 41 | return string 42 | -------------------------------------------------------------------------------- /image/combine.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ..image.prefix import get_image_prefix 4 | 5 | 6 | def get_combined_images(img1, img2, from_channel, to_channel) -> np.ndarray: 7 | n = 4 8 | size = img1.size[0] * img1.size[1] 9 | a = np.array(img1.pixels).reshape(size, n) 10 | b = np.array(img2.pixels).reshape(size, n) 11 | a[:, to_channel] = b[:, from_channel] # numpy magic happens here 12 | return a.reshape(size * n) 13 | 14 | 15 | def combine_channels_to_image(target_image, 16 | R=None, G=None, B=None, A=None, 17 | channel_r=0, channel_g=0, channel_b=0, channel_a=0, 18 | invert_r=False, invert_g=False, invert_b=False, invert_a=False): 19 | """Combine image channels into RGBA-channels of target image.""" 20 | 21 | n = 4 22 | t = np.array(target_image.pixels) 23 | 24 | if R: 25 | t[0::n] = np.array(R.pixels)[channel_r::n] 26 | if G: 27 | t[1::n] = np.array(G.pixels)[channel_g::n] 28 | if B: 29 | t[2::n] = np.array(B.pixels)[channel_b::n] 30 | if A: 31 | t[3::n] = np.array(A.pixels)[channel_a::n] 32 | 33 | if invert_r: 34 | t[0::n] = 1 - t[0::n] 35 | if invert_g: 36 | t[1::n] = 1 - t[1::n] 37 | if invert_b: 38 | t[2::n] = 1 - t[2::n] 39 | if invert_a: 40 | t[3::n] = 1 - t[3::n] 41 | 42 | target_image.pixels = t 43 | -------------------------------------------------------------------------------- /image/invert.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def get_invert_image(img) -> np.ndarray: 5 | n = 4 6 | size = img.size[0] * img.size[1] 7 | a = np.array(img.pixels).reshape(size, n) 8 | a[:, 0:3] = 1 - a[:, 0:3] 9 | return a.reshape(size * n) 10 | -------------------------------------------------------------------------------- /image/prefix.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..functions import remove_not_allowed_signs 4 | from ..material.has_material import has_material 5 | 6 | 7 | def get_image_prefix(object_name): 8 | """:returns: Image prefix by object name and altered by user settings.""" 9 | 10 | settings = bpy.context.scene.principled_baker_settings 11 | prefix = settings.image_prefix 12 | object_name = remove_not_allowed_signs(object_name) 13 | 14 | if settings.use_first_material_name: 15 | if has_material(bpy.data.objects[object_name]): 16 | prefix += bpy.data.objects[object_name].material_slots[0].material.name 17 | return prefix 18 | -------------------------------------------------------------------------------- /image/save.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..image.save_as import save_image_as 4 | 5 | 6 | def save_image(image, jobname): 7 | """Wrapper for save_image_as() to get all parameters from settings.""" 8 | 9 | settings = bpy.context.scene.principled_baker_settings 10 | 11 | if jobname == 'Color' and settings.use_alpha_to_color: 12 | color_mode = 'RGBA' 13 | else: 14 | color_mode = settings.color_mode 15 | 16 | # color depth 17 | color_depth = settings.color_depth 18 | if color_depth == 'INDIVIDUAL': 19 | bakelist = bpy.context.scene.principled_baker_bakelist 20 | if jobname in bakelist.keys(): 21 | color_depth = bakelist[jobname].color_depth 22 | else: 23 | if jobname == "Diffuse": 24 | color_depth = settings.color_depth_diffuse 25 | elif jobname == "Bump": 26 | color_depth = settings.color_depth_bump 27 | elif jobname == "Vertex Color": 28 | color_depth = settings.color_depth_vertex_color 29 | elif jobname == "Material ID": 30 | color_depth = settings.color_depth_material_id 31 | elif jobname == "Wireframe": 32 | color_depth = settings.color_depth_wireframe 33 | 34 | save_image_as(image, 35 | file_path=image.filepath, 36 | file_format=settings.file_format, 37 | color_mode=color_mode, 38 | color_depth=color_depth, 39 | compression=settings.compression, 40 | quality=settings.quality, 41 | tiff_codec=settings.tiff_codec, 42 | exr_codec=settings.exr_codec) 43 | -------------------------------------------------------------------------------- /image/save_as.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def save_image_as(image, file_path, file_format, color_mode='RGB', color_depth='8', compression=15, quality=90, tiff_codec='DEFLATE', exr_codec='ZIP'): 5 | s = bpy.context.scene.render.image_settings 6 | fm = s.file_format 7 | cm = s.color_mode 8 | cd = s.color_depth 9 | c = s.compression 10 | q = s.quality 11 | tc = s.tiff_codec 12 | ec = s.exr_codec 13 | vt = bpy.context.scene.view_settings.view_transform 14 | 15 | s.file_format = file_format 16 | s.color_mode = color_mode 17 | s.color_depth = color_depth 18 | s.compression = compression 19 | s.quality = quality 20 | s.tiff_codec = tiff_codec 21 | s.exr_codec = exr_codec 22 | defalut_vt = 'Standard' 23 | bpy.context.scene.view_settings.view_transform = defalut_vt 24 | 25 | image.use_view_as_render = False 26 | 27 | abs_path = bpy.path.abspath(file_path) 28 | 29 | image.save_render(abs_path) 30 | 31 | s.file_format = fm 32 | s.color_mode = cm 33 | s.color_depth = cd 34 | s.compression = c 35 | s.quality = q 36 | s.tiff_codec = tc 37 | s.exr_codec = ec 38 | bpy.context.scene.view_settings.view_transform = vt 39 | -------------------------------------------------------------------------------- /image/suffix.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def get_image_suffix(jobname): 5 | """:returns: Image suffix by job name and altered by user settings.""" 6 | 7 | settings = bpy.context.scene.principled_baker_settings 8 | suffixlist = bpy.context.scene.principled_baker_suffixlist 9 | suffix = suffixlist[jobname]['suffix'] 10 | 11 | if settings.suffix_text_mod == 'lower': 12 | suffix = suffix.lower() 13 | elif settings.suffix_text_mod == 'upper': 14 | suffix = suffix.upper() 15 | elif settings.suffix_text_mod == 'title': 16 | suffix = suffix.title() 17 | return suffix 18 | -------------------------------------------------------------------------------- /joblist.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .const import ALPHA_NODES, BSDF_NODES, MATERIAL_TAG, NODE_INPUTS, NODE_TAG 4 | from .functions import is_list_equal 5 | from .material import has_material 6 | from .material.has_material import has_material 7 | from .nodes.delete_tagged import delete_tagged_nodes_in_object 8 | from .nodes.node import are_nodes_connected, is_node_type_in_node_tree 9 | from .nodes.value_list import get_value_from_node_by_name 10 | from .prepare.material import prepare_material_for_bake 11 | 12 | 13 | def get_value_list_from_bsdf_nodes_in_material(material, node, value_name) -> list: 14 | value_list = [] 15 | for n in material.node_tree.nodes: 16 | if n.type in BSDF_NODES: 17 | if are_nodes_connected(n, node): 18 | val = get_value_from_node_by_name(n, value_name) 19 | if val is not None: 20 | value_list.append(val) 21 | return value_list 22 | 23 | 24 | def get_joblist_by_bake_list() -> list: 25 | joblist = list() 26 | 27 | bakelist = bpy.context.scene.principled_baker_bakelist 28 | for jobname, data in bakelist.items(): 29 | if data.do_bake: 30 | joblist.append(jobname) 31 | 32 | return joblist 33 | 34 | 35 | def get_joblist_by_additional_bake_types() -> list: 36 | """To extend the joblist by user settings: 37 | 38 | Diffuse, Glossiness, Bump, Material ID, Wireframe 39 | """ 40 | 41 | settings = bpy.context.scene.principled_baker_settings 42 | joblist = [] 43 | if settings.use_Diffuse: 44 | joblist.append("Diffuse") 45 | if settings.use_invert_roughness: 46 | joblist.append("Roughness") 47 | if settings.use_Bump: 48 | joblist.append("Bump") 49 | if settings.use_material_id: 50 | joblist.append("Material ID") 51 | if settings.use_wireframe: 52 | joblist.append("Wireframe") 53 | return joblist 54 | 55 | 56 | def get_vertex_colors_to_bake_from_objects(objs) -> list: 57 | settings = bpy.context.scene.principled_baker_settings 58 | vert_col_names = [] 59 | 60 | if settings.use_vertex_color: 61 | vert_col_names = [] 62 | for obj in objs: 63 | if len(obj.data.vertex_colors) == 0: 64 | continue 65 | 66 | if settings.bake_vertex_colors == 'ALL': 67 | for vcol in obj.data.vertex_colors: 68 | vert_col_names.append(vcol.name) 69 | elif settings.bake_vertex_colors == 'SELECTED': 70 | vert_col_names.append(obj.data.vertex_colors.active.name) 71 | elif settings.bake_vertex_colors == 'ACTIVE_RENDER': 72 | for _, v_col in obj.data.vertex_colors.items(): 73 | if v_col.active_render: 74 | vert_col_names.append(v_col.name) 75 | break 76 | else: 77 | index = int(settings.bake_vertex_colors) - 1 78 | if index < len(obj.data.vertex_colors): 79 | vert_col_names.append( 80 | obj.data.vertex_colors[index].name) 81 | 82 | return vert_col_names 83 | 84 | 85 | def get_joblist_by_value_differ_from_objects(objs) -> list: 86 | # settings = bpy.context.scene.principled_baker_settings 87 | joblist = list() 88 | 89 | for value_name in NODE_INPUTS: 90 | value_list = list() 91 | if value_name not in joblist and value_name not in {'Subsurface Radius', 'Normal', 'Clearcoat Normal', 'Tangent'}: 92 | for obj in objs: 93 | for mat_slot in obj.material_slots: 94 | if mat_slot.material: 95 | mat = mat_slot.material 96 | 97 | material_output = None 98 | for node in mat.node_tree.nodes: 99 | if node.type == "OUTPUT_MATERIAL" and NODE_TAG in node.keys(): 100 | material_output = node 101 | 102 | if material_output: 103 | value_list.extend(get_value_list_from_bsdf_nodes_in_material( 104 | mat, material_output, value_name)) 105 | if value_list: 106 | if not is_list_equal(value_list): 107 | joblist.append(value_name) 108 | 109 | return joblist 110 | 111 | 112 | def get_joblist_by_connected_inputs_from_objects(objs) -> list: 113 | settings = bpy.context.scene.principled_baker_settings 114 | joblist = list() 115 | 116 | # search material for jobs by connected node types 117 | for obj in objs: 118 | for mat_slot in obj.material_slots: 119 | if mat_slot.material: 120 | mat = mat_slot.material 121 | 122 | if MATERIAL_TAG not in mat_slot.material.keys(): 123 | material_output = None 124 | for node in mat.node_tree.nodes: 125 | if node.type == "OUTPUT_MATERIAL" and NODE_TAG in node.keys(): 126 | material_output = node 127 | 128 | if material_output: 129 | # add special cases: 130 | # Alpha node: Transparent 131 | if is_node_type_in_node_tree(mat, material_output, 'BSDF_TRANSPARENT'): 132 | # if not 'Alpha' in joblist: 133 | joblist.append('Alpha') 134 | 135 | # Alpha for nodes: Translucent, Glass 136 | for alpha_name, n_type in ALPHA_NODES.items(): 137 | if is_node_type_in_node_tree(mat, material_output, n_type): 138 | # if not alpha_name in joblist: 139 | joblist.append(alpha_name) 140 | 141 | # Emission 142 | if is_node_type_in_node_tree(mat, material_output, 'EMISSION'): 143 | # if not 'Emission' in joblist: 144 | joblist.append('Emission') 145 | 146 | # AO 147 | if is_node_type_in_node_tree(mat, material_output, 'AMBIENT_OCCLUSION'): 148 | # if not 'Ambient Occlusion' in joblist: 149 | joblist.append('Ambient Occlusion') 150 | 151 | # Displacement 152 | socket_name = 'Displacement' 153 | if material_output.inputs[socket_name].is_linked: 154 | joblist.append(socket_name) 155 | 156 | # Bump 157 | socket_name = 'Bump' 158 | if settings.use_Bump and is_node_type_in_node_tree(mat, material_output, 'BUMP'): 159 | if socket_name not in joblist: 160 | joblist.append(socket_name) 161 | 162 | # BSDF nodes 163 | for node in mat.node_tree.nodes: 164 | if NODE_TAG in node.keys(): 165 | if node.type in BSDF_NODES: 166 | for socket in node.inputs: 167 | socket_name = socket.name 168 | if socket_name in NODE_INPUTS + ['Base Color']: 169 | if socket.is_linked: 170 | if are_nodes_connected(node, material_output): 171 | socket_name = 'Color' if socket_name == 'Base Color' else socket_name 172 | # if not socket_name in joblist: 173 | joblist.append(socket_name) 174 | 175 | return joblist 176 | 177 | 178 | def get_joblist_by_autodetection_from_objects(objs) -> list: 179 | 180 | joblist = [] 181 | 182 | # Prepare materials - see clean up! 183 | materials = [] 184 | for obj in objs: 185 | for mat_slot in obj.material_slots: 186 | if mat_slot.material: 187 | materials.append(mat_slot.material) 188 | materials = set(materials) 189 | for mat in materials: 190 | prepare_material_for_bake(mat, do_ungroup_values=False) 191 | 192 | settings = bpy.context.scene.principled_baker_settings 193 | if settings.use_value_differ: 194 | joblist.extend(get_joblist_by_value_differ_from_objects(objs)) 195 | 196 | if settings.use_connected_inputs: 197 | joblist.extend(get_joblist_by_connected_inputs_from_objects(objs)) 198 | 199 | # Clean up! - delete temp nodes 200 | for obj in objs: 201 | delete_tagged_nodes_in_object(obj) 202 | 203 | return joblist 204 | 205 | 206 | def get_joblist_from_objects(objs) -> list: 207 | settings = bpy.context.scene.principled_baker_settings 208 | joblist = [] 209 | 210 | # if one object has no material, the joblist has only "Normal"! 211 | if settings.bake_mode == 'SELECTED_TO_ACTIVE': 212 | for obj in objs: 213 | if not has_material(obj): 214 | joblist.append("Normal") 215 | return joblist 216 | 217 | if settings.use_autodetect: 218 | joblist.extend(get_joblist_by_autodetection_from_objects(objs)) 219 | 220 | # force bake of Color, if user wants alpha in color 221 | if settings.use_alpha_to_color and settings.color_mode == 'RGBA': 222 | joblist.append('Color') 223 | joblist.append('Alpha') 224 | 225 | if not settings.use_autodetect: 226 | joblist.extend(get_joblist_by_bake_list()) 227 | 228 | joblist.extend(get_joblist_by_additional_bake_types()) 229 | 230 | if settings.use_vertex_color: 231 | joblist.append("Vertex Color") 232 | 233 | joblist = list(set(joblist)) 234 | 235 | return joblist 236 | -------------------------------------------------------------------------------- /material/add_images.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..const import (ALPHA_NODES, NODE_INPUTS_SORTED, 4 | NORMAL_INPUTS) 5 | from ..nodes.find import find_node_by_type 6 | from ..nodes.new import new_image_node, new_mixrgb_node 7 | from ..nodes.node import get_sibling_node 8 | 9 | 10 | def add_images_to_material(new_images, new_mat): 11 | """Add new images to new material. Nodes will be linked and arranged.""" 12 | 13 | settings = bpy.context.scene.principled_baker_settings 14 | 15 | def new_link(from_socket, to_socket): 16 | new_mat.node_tree.links.new(from_socket, to_socket) 17 | 18 | NOT_TO_LINK_NODES = {"Glossiness", "Ambient Occlusion", 19 | "Vertex Color", "Material ID", "Diffuse", "Wireframe"} 20 | 21 | NEW_NODE_COLORS = { 22 | "Alpha": [1.0, 1.0, 1.0, 1.0], 23 | "Translucent_Alpha": [0.8, 0.8, 0.8, 1.0], 24 | "Glass_Alpha": [1.0, 1.0, 1.0, 1.0], 25 | 'Emission': [1.0, 1.0, 1.0, 1.0], 26 | } 27 | 28 | NODE_OFFSET_X = 300 29 | NODE_OFFSET_Y = 200 30 | 31 | IMAGE_NODE_OFFSET_X = -900 32 | IMAGE_NODE_OFFSET_Y = -260 33 | IMAGE_NODE_WIDTH = 300 34 | 35 | principled_node = find_node_by_type(new_mat, 'BSDF_PRINCIPLED') 36 | material_output = find_node_by_type(new_mat, 'OUTPUT_MATERIAL') 37 | 38 | nodes = new_mat.node_tree.nodes 39 | 40 | if new_images: 41 | tex_coord_node = nodes.new(type="ShaderNodeTexCoord") 42 | mapping_node = nodes.new(type="ShaderNodeMapping") 43 | new_link(tex_coord_node.outputs["UV"], 44 | mapping_node.inputs["Vector"]) 45 | mapping_node.location.x = principled_node.location.x + \ 46 | IMAGE_NODE_OFFSET_X - mapping_node.width - 100 47 | tex_coord_node.location.x = mapping_node.location.x - tex_coord_node.width - 100 48 | 49 | # sort images nicely for new material 50 | images_sorted = {} 51 | images_rest = {} # for Vertex Colors 52 | for jobname in NODE_INPUTS_SORTED: 53 | if (jobname in new_images.keys()): 54 | images_sorted[jobname] = new_images[jobname] 55 | for jobname in set(new_images) - set(images_sorted): 56 | images_rest[jobname] = new_images[jobname] 57 | images_sorted.update(images_rest) 58 | 59 | image_nodes = {} 60 | node_offset_index = 0 61 | for name, image in images_sorted.items(): 62 | image_node = new_image_node(new_mat) 63 | image_node.label = name 64 | 65 | image_nodes[name] = image_node 66 | image_node.image = image 67 | 68 | # link to mapping node 69 | new_link(mapping_node.outputs["Vector"], 70 | image_node.inputs["Vector"]) 71 | 72 | # rearrange nodes 73 | image_node.width = IMAGE_NODE_WIDTH 74 | image_node.location.x = principled_node.location.x + IMAGE_NODE_OFFSET_X 75 | image_node.location.y = principled_node.location.y + \ 76 | IMAGE_NODE_OFFSET_Y * node_offset_index 77 | node_offset_index += 1 78 | 79 | # link nodes 80 | for name, image_node in image_nodes.items(): 81 | if name in NORMAL_INPUTS: 82 | normal_node = nodes.new(type="ShaderNodeNormalMap") 83 | normal_node.location.x = IMAGE_NODE_OFFSET_X + 1.5 * IMAGE_NODE_WIDTH 84 | normal_node.location.y = image_nodes[name].location.y 85 | new_link(image_node.outputs['Color'], 86 | normal_node.inputs['Color']) 87 | new_link(normal_node.outputs[name], 88 | principled_node.inputs[name]) 89 | 90 | elif name == 'Bump': 91 | bump_node = nodes.new(type="ShaderNodeBump") 92 | bump_node.location.x = IMAGE_NODE_OFFSET_X + 1.5 * IMAGE_NODE_WIDTH 93 | bump_node.location.y = image_nodes[name].location.y 94 | new_link(image_node.outputs['Color'], 95 | bump_node.inputs['Height']) 96 | new_link(bump_node.outputs['Normal'], 97 | principled_node.inputs['Normal']) 98 | 99 | elif name == "Displacement": 100 | disp_node = nodes.new(type='ShaderNodeDisplacement') 101 | new_link(image_node.outputs['Color'], 102 | disp_node.inputs["Height"]) 103 | new_link(disp_node.outputs["Displacement"], 104 | material_output.inputs["Displacement"]) 105 | disp_node.location.x = NODE_OFFSET_X 106 | 107 | elif name in ALPHA_NODES.keys(): 108 | if name == "Translucent_Alpha": 109 | alpha_node = nodes.new(type='ShaderNodeBsdfTranslucent') 110 | elif name == "Glass_Alpha": 111 | alpha_node = nodes.new(type='ShaderNodeBsdfGlass') 112 | 113 | # color 114 | alpha_node.inputs['Color'].default_value = NEW_NODE_COLORS[name] 115 | 116 | mixshader_node = nodes.new(type='ShaderNodeMixShader') 117 | 118 | # links 119 | new_link(material_output.inputs[0].links[0].from_socket, 120 | mixshader_node.inputs[2]) 121 | new_link(mixshader_node.outputs['Shader'], 122 | material_output.inputs[0]) 123 | new_link(alpha_node.outputs['BSDF'], 124 | mixshader_node.inputs[1]) 125 | if not settings.use_alpha_to_color: 126 | new_link(image_node.outputs['Color'], 127 | mixshader_node.inputs['Fac']) 128 | 129 | # node locations 130 | sib = get_sibling_node(alpha_node) 131 | alpha_node.location = ( 132 | sib.location.x, sib.location.y + NODE_OFFSET_Y) 133 | mid_offset_y = alpha_node.location.y 134 | mixshader_node.location = ( 135 | sib.location.x + NODE_OFFSET_X, mid_offset_y) 136 | 137 | elif name == "Alpha": 138 | new_link(image_node.outputs['Color'], 139 | principled_node.inputs['Alpha']) 140 | 141 | elif name == 'Emission': 142 | new_link(image_node.outputs['Color'], 143 | principled_node.inputs['Emission']) 144 | 145 | elif name == 'Color': 146 | name = 'Base Color' 147 | new_link(image_node.outputs['Color'], 148 | principled_node.inputs[name]) 149 | 150 | if settings.use_alpha_to_color and "Alpha" in new_images.keys(): 151 | new_link(image_node.outputs['Alpha'], 152 | principled_node.inputs['Alpha']) 153 | 154 | # mix AO with color 155 | if 'Ambient Occlusion' in image_nodes.keys(): 156 | ao_image_node = image_nodes['Ambient Occlusion'] 157 | 158 | # mix 159 | mix_node = new_mixrgb_node(new_mat, fac=1.0) 160 | mix_node.blend_type = 'MULTIPLY' 161 | mix_node.location.x = image_node.location.x - IMAGE_NODE_OFFSET_X / 2 162 | mix_node.location.y = image_node.location.y 163 | 164 | # links 165 | new_link(mix_node.outputs["Color"], 166 | principled_node.inputs['Base Color']) 167 | new_link(image_node.outputs["Color"], 168 | mix_node.inputs['Color1']) 169 | new_link(ao_image_node.outputs["Color"], 170 | mix_node.inputs['Color2']) 171 | 172 | elif name in NOT_TO_LINK_NODES or name.startswith("Vertex Color"): 173 | pass # skip some 174 | 175 | # TODO link single baked images as option 176 | if len(image_nodes) == 1: 177 | new_link(image_node.outputs['Color'], 178 | principled_node.inputs["Base Color"]) 179 | 180 | else: 181 | new_link(image_node.outputs['Color'], 182 | principled_node.inputs[name]) 183 | -------------------------------------------------------------------------------- /material/add_temp_material.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..const import MATERIAL_TAG_VERTEX 4 | from ..nodes.find import find_node_by_type 5 | 6 | 7 | def add_temp_material(obj): 8 | import time 9 | name = f"PRINCIPLED_BAKER_TEMP_MATERIAL_{time.time()}" 10 | mat = bpy.data.materials.new(name) 11 | mat[MATERIAL_TAG_VERTEX] = 1 12 | mat.use_nodes = True 13 | principled_node = find_node_by_type(mat, 'BSDF_PRINCIPLED') 14 | principled_node.inputs["Base Color"].default_value = [0, 0, 0, 1] 15 | obj.data.materials.append(mat) 16 | -------------------------------------------------------------------------------- /material/delete_tagged_materials.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..const import MATERIAL_TAG, MATERIAL_TAG_VERTEX 4 | from ..nodes.find import find_node_by_type 5 | from ..nodes.outputs import get_active_output 6 | 7 | 8 | def delete_tagged_materials(obj, tag): 9 | for i, mat_slot in enumerate(obj.material_slots): 10 | if mat_slot.material: 11 | if tag in mat_slot.material.keys(): 12 | bpy.context.object.active_material_index = i 13 | bpy.ops.object.material_slot_remove({'object': obj}) 14 | -------------------------------------------------------------------------------- /material/has_material.py: -------------------------------------------------------------------------------- 1 | from ..const import MATERIAL_TAG 2 | from ..nodes.outputs import get_active_output 3 | 4 | 5 | def has_material(obj): 6 | if len(obj.material_slots) >= 1: 7 | for mat_slot in obj.material_slots: 8 | if mat_slot.material: 9 | if MATERIAL_TAG not in mat_slot.material.keys(): 10 | material_output = get_active_output(mat_slot.material) 11 | if material_output is None: 12 | return False 13 | else: 14 | if not material_output.inputs['Surface'].is_linked: 15 | return False 16 | else: 17 | return True 18 | else: 19 | return False 20 | -------------------------------------------------------------------------------- /material/new.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..const import MATERIAL_TAG 4 | 5 | 6 | def new_material(name, principled_node_values=None): 7 | """:returns: Object reference to new material.""" 8 | 9 | mat = bpy.data.materials.new(name) 10 | mat.use_nodes = True 11 | mat[MATERIAL_TAG] = 1 12 | 13 | mat_output = mat.node_tree.nodes['Material Output'] 14 | mat_output.location = (300.0, 300.0) 15 | 16 | principled_node = mat.node_tree.nodes['Principled BSDF'] 17 | 18 | principled_node.location = (10.0, 300.0) 19 | 20 | # copy settings to new principled_node 21 | if not principled_node_values == None: 22 | for name, val in principled_node_values.items(): 23 | if name == 'Color': 24 | name = 'Base Color' 25 | if val is not None: 26 | principled_node.inputs[name].default_value = val 27 | 28 | mat.node_tree.links.new( 29 | principled_node.outputs['BSDF'], mat_output.inputs['Surface']) 30 | return mat 31 | -------------------------------------------------------------------------------- /nodes/delete_tagged.py: -------------------------------------------------------------------------------- 1 | from ..const import NODE_TAG 2 | 3 | 4 | def delete_tagged_nodes(material): 5 | for node in material.node_tree.nodes: 6 | if NODE_TAG in node.keys(): 7 | material.node_tree.nodes.remove(node) 8 | 9 | 10 | def delete_tagged_nodes_in_object(obj): 11 | for mat_slot in obj.material_slots: 12 | if mat_slot.material: 13 | delete_tagged_nodes(mat_slot.material) 14 | -------------------------------------------------------------------------------- /nodes/duplicate.py: -------------------------------------------------------------------------------- 1 | from ..nodes.socket_index import * 2 | 3 | 4 | def duplicate_node(mat, node): 5 | 6 | node_type = str(type(node)).split('.')[-1][:-2] 7 | new_node = mat.node_tree.nodes.new(type=node_type) 8 | 9 | # copy attributes 10 | for attr in dir(node): 11 | try: 12 | a = getattr(node, attr) 13 | setattr(new_node, attr, a) 14 | except AttributeError: 15 | pass 16 | 17 | # Color Ramp 18 | if node.type == 'VALTORGB': 19 | for attr in dir(node.color_ramp): 20 | try: 21 | a = getattr(node.color_ramp, attr) 22 | setattr(new_node.color_ramp, attr, a) 23 | except AttributeError: 24 | pass 25 | 26 | for i, col_ramp_elem in enumerate(node.color_ramp.elements): 27 | try: 28 | new_node.color_ramp.elements[i].color = col_ramp_elem.color 29 | new_node.color_ramp.elements[i].position = col_ramp_elem.position 30 | except IndexError: 31 | pos = col_ramp_elem.position 32 | new_elem = new_node.color_ramp.elements.new(pos) 33 | new_elem.color = col_ramp_elem.color 34 | 35 | # Curve 36 | if node.type == 'CURVE_RGB': 37 | for attr in dir(node.mapping): 38 | try: 39 | a = getattr(node.mapping, attr) 40 | setattr(new_node.mapping, attr, a) 41 | except AttributeError: 42 | pass 43 | 44 | # copy every point in every curve 45 | for i, curve in enumerate(node.mapping.curves): 46 | for j, point in enumerate(curve.points): 47 | try: 48 | new_node.mapping.curves[i].points[j].location = point.location 49 | new_node.mapping.curves[i].points[j].handle_type = point.handle_type 50 | except IndexError: 51 | pos = point.location[0] 52 | val = point.location[1] 53 | new_node.mapping.curves[i].points.new(pos, val) 54 | 55 | # copy values inputs 56 | for i, input in enumerate(node.inputs): 57 | try: 58 | new_node.inputs[i].default_value = input.default_value 59 | except: 60 | pass 61 | 62 | # copy values outputs 63 | for i, output in enumerate(node.outputs): 64 | try: 65 | new_node.outputs[i].default_value = output.default_value 66 | except: 67 | pass 68 | 69 | return new_node 70 | 71 | 72 | def duplicate_nodes(mat, nodes, keep_inputs=False): 73 | 74 | new_nodes = {} 75 | 76 | for node in set(nodes): 77 | new_node = duplicate_node(mat, node) 78 | new_nodes[node] = new_node 79 | 80 | if keep_inputs: 81 | for node, new_node in new_nodes.items(): 82 | len_inputs = len(node.inputs) 83 | if len_inputs > 0: 84 | for i, input in enumerate(node.inputs): 85 | if input.is_linked: 86 | link = input.links[0] 87 | from_node = link.from_node 88 | to_socket = new_node.inputs[i] 89 | if from_node in new_nodes.keys(): 90 | from_socket_index = socket_index(link.from_socket) 91 | from_socket = new_nodes[from_node].outputs[from_socket_index] 92 | else: 93 | from_socket = link.from_socket 94 | mat.node_tree.links.new(from_socket, to_socket) 95 | 96 | return list(new_nodes.values()) 97 | -------------------------------------------------------------------------------- /nodes/find.py: -------------------------------------------------------------------------------- 1 | def find_node_by_type(mat, node_type): 2 | for node in mat.node_tree.nodes: 3 | if node.type == node_type: 4 | return node 5 | -------------------------------------------------------------------------------- /nodes/new.py: -------------------------------------------------------------------------------- 1 | from ..const import NODE_TAG 2 | 3 | 4 | def new_pb_emission_node(material, color=[0, 0, 0, 1]): 5 | node = material.node_tree.nodes.new(type='ShaderNodeEmission') 6 | node.inputs['Color'].default_value = color # [0, 0, 0, 1] 7 | node[NODE_TAG] = 1 8 | return node 9 | 10 | 11 | def new_pb_output_node(material): 12 | node = material.node_tree.nodes.new(type='ShaderNodeOutputMaterial') 13 | node.is_active_output = True 14 | node[NODE_TAG] = 1 15 | return node 16 | 17 | 18 | def new_rgb_node(mat, color=(0, 0, 0, 1)): 19 | node = mat.node_tree.nodes.new(type="ShaderNodeRGB") 20 | node[NODE_TAG] = 1 21 | node.outputs['Color'].default_value = color 22 | node.color = (0.8, 0.8, 0.8) 23 | node.use_custom_color = True 24 | return node 25 | 26 | 27 | def new_mixrgb_node(mat, fac=0.5, color1=[0, 0, 0, 1], color2=[0, 0, 0, 1]): 28 | node = mat.node_tree.nodes.new(type="ShaderNodeMixRGB") 29 | node[NODE_TAG] = 1 30 | node.inputs[0].default_value = fac 31 | node.inputs[1].default_value = color1 32 | node.inputs[2].default_value = color2 33 | node.color = (0.8, 0.8, 0.8) 34 | node.use_custom_color = True 35 | return node 36 | 37 | 38 | def new_image_node(material): 39 | image_node = material.node_tree.nodes.new(type="ShaderNodeTexImage") 40 | return image_node 41 | 42 | 43 | def create_bake_image_node(mat, image): 44 | bake_image_node = new_image_node(mat) 45 | bake_image_node.image = image # add image to node 46 | bake_image_node[NODE_TAG] = 1 # tag for clean up 47 | bake_image_node.label = "TEMP BAKE NODE (If you see this, something went wrong!)" 48 | bake_image_node.use_custom_color = True 49 | bake_image_node.color = (1, 0, 0) 50 | 51 | # make only bake_image_node active 52 | bake_image_node.select = True 53 | mat.node_tree.nodes.active = bake_image_node 54 | 55 | 56 | def create_bake_image_nodes(objects, image): 57 | for obj in objects: 58 | for mat_slot in obj.material_slots: 59 | if mat_slot.material: 60 | create_bake_image_node(mat_slot.material, image) 61 | -------------------------------------------------------------------------------- /nodes/node.py: -------------------------------------------------------------------------------- 1 | def get_all_nodes_linked_from(node): 2 | nodes = [] 3 | 4 | def linked_from(node): 5 | if node: 6 | nodes.append(node) 7 | for input_socket in node.inputs: 8 | if input_socket.is_linked: 9 | from_node = input_socket.links[0].from_node 10 | linked_from(from_node) 11 | linked_from(node) 12 | return nodes 13 | 14 | 15 | def is_mixnode_in_node_tree(node): 16 | """return True only if mix node is higher in tree""" 17 | 18 | node_type = 'MIX_RGB' 19 | if node.type == node_type: 20 | return True 21 | elif node.type == 'AMBIENT_OCCLUSION': 22 | return False 23 | else: 24 | for input_socket in node.inputs: 25 | if input_socket.is_linked: 26 | from_node = input_socket.links[0].from_node 27 | if is_mixnode_in_node_tree(from_node): 28 | return True 29 | return False 30 | 31 | 32 | def are_nodes_connected(node, to_node): 33 | if node == to_node: 34 | return True 35 | for output_socket in node.outputs: 36 | if output_socket.is_linked: 37 | to_node_tmp = output_socket.links[0].to_node 38 | return are_nodes_connected(to_node_tmp, to_node) 39 | 40 | 41 | def get_sibling_node(node): 42 | if node.outputs[0].is_linked: 43 | parent_node = node.outputs[0].links[0].to_node 44 | for input_socket in parent_node.inputs: 45 | if input_socket.is_linked: 46 | child_node = input_socket.links[0].from_node 47 | if not child_node == node: 48 | if input_socket.type == node.outputs[0].links[0].to_socket.type: 49 | return child_node 50 | 51 | 52 | def is_node_type_in_node_tree(material, node, node_type): 53 | for n in material.node_tree.nodes: 54 | if n.type == node_type: 55 | if are_nodes_connected(n, node): 56 | return True 57 | -------------------------------------------------------------------------------- /nodes/outputs.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..const import MATERIAL_TAG 4 | 5 | 6 | def deactivate_material_outputs(material): 7 | for node in material.node_tree.nodes: 8 | if node.type == "OUTPUT_MATERIAL": 9 | node.is_active_output = False 10 | 11 | 12 | def get_active_output(mat) -> bpy.types.Node: 13 | for node in mat.node_tree.nodes: 14 | if node.type == "OUTPUT_MATERIAL" and node.is_active_output: 15 | return node 16 | 17 | 18 | def get_active_outputs(objects) -> list: 19 | if not isinstance(objects, list): 20 | objects = [objects] 21 | 22 | active_outputs = [] 23 | for obj in objects: 24 | for mat_slot in obj.material_slots: 25 | if mat_slot.material: 26 | if MATERIAL_TAG not in mat_slot.material.keys(): 27 | node = get_active_output(mat_slot.material) 28 | if node: 29 | active_outputs.append(node) 30 | return active_outputs 31 | 32 | 33 | def get_all_material_outputs(objects) -> dict: 34 | if not isinstance(objects, list): 35 | objects = [objects] 36 | 37 | outputs = {} 38 | for obj in objects: 39 | for mat_slot in obj.material_slots: 40 | if mat_slot.material: 41 | for node in mat_slot.material.node_tree.nodes: 42 | if node.type == "OUTPUT_MATERIAL": 43 | outputs[node] = node.target 44 | return outputs 45 | 46 | 47 | def set_material_outputs_target_to_all(objects): 48 | if not isinstance(objects, list): 49 | objects = [objects] 50 | 51 | for obj in objects: 52 | for mat_slot in obj.material_slots: 53 | if mat_slot.material: 54 | for node in mat_slot.material.node_tree.nodes: 55 | if node.type == "OUTPUT_MATERIAL": 56 | node.target = 'ALL' 57 | -------------------------------------------------------------------------------- /nodes/principled_node.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..const import MATERIAL_TAG, NODE_INPUTS 4 | from ..functions import is_list_equal 5 | from ..nodes.outputs import get_active_output 6 | from ..nodes.value_list import get_value_from_node_by_name 7 | 8 | 9 | def get_value_list(node, value_name, node_type): 10 | """ 11 | :returns: List of all values by value name in a node tree starting from node. 12 | Values from Normal Map are exclued. 13 | """ 14 | 15 | value_list = [] 16 | 17 | def find_values(node, value_name): 18 | if node.type == node_type: 19 | val = get_value_from_node_by_name(node, value_name) 20 | if val is not None: 21 | value_list.append(val) 22 | 23 | for socket in node.inputs: 24 | if socket.is_linked: 25 | from_node = socket.links[0].from_node 26 | find_values(from_node, value_name) 27 | 28 | find_values(node, value_name) 29 | return value_list 30 | 31 | 32 | def get_principled_node_values(objs): 33 | # TODO bug: ignores nodes in groups 34 | """:returns: Dictionary with equal node values in all materials in all objects, eg. metal, roughness, etc.""" 35 | 36 | settings = bpy.context.scene.principled_baker_settings 37 | 38 | pri_node_values = {} 39 | for obj in objs: 40 | for value_name in NODE_INPUTS + ["Base Color"]: 41 | 42 | if value_name not in {'Subsurface Radius', 'Normal', 'Clearcoat Normal', 'Tangent'}: 43 | value_list = [] 44 | for mat_slot in obj.material_slots: 45 | if mat_slot.material: 46 | mat = mat_slot.material 47 | if MATERIAL_TAG not in mat.keys(): 48 | material_output = get_active_output(mat) 49 | tmp_val_list = get_value_list( 50 | material_output, value_name, 'BSDF_PRINCIPLED') 51 | value_list.extend(tmp_val_list) 52 | 53 | if value_list: 54 | if is_list_equal(value_list): 55 | if settings.make_new_material or settings.bake_mode == 'SELECTED_TO_ACTIVE' or settings.duplicate_objects: 56 | pri_node_values[value_name] = value_list[0] 57 | 58 | return pri_node_values 59 | -------------------------------------------------------------------------------- /nodes/socket_index.py: -------------------------------------------------------------------------------- 1 | def socket_index(socket): 2 | node = socket.node 3 | sockets = node.outputs if socket.is_output else node.inputs 4 | for i, s in enumerate(sockets): 5 | if s.is_linked: 6 | if socket == s: 7 | return i 8 | -------------------------------------------------------------------------------- /nodes/ungroup.py: -------------------------------------------------------------------------------- 1 | from ..nodes.duplicate import duplicate_node 2 | from ..nodes.socket_index import * 3 | 4 | 5 | def ungroup_nodes(mat, group_nodes, do_ungroup_values=True): 6 | 7 | new_nodes = {} 8 | val_nodes = [] 9 | 10 | def duplicate_from_input_socket(mat, input_socket, link_to_socket): 11 | if not input_socket: 12 | return 13 | old_node = input_socket.links[0].from_node 14 | old_from_socket = input_socket.links[0].from_socket 15 | if old_node.type == 'GROUP_INPUT': 16 | 17 | # link 18 | index_in = socket_index(old_from_socket) 19 | if group_node.inputs[index_in].is_linked: 20 | from_socket = group_node.inputs[index_in].links[0].from_socket 21 | to_socket = link_to_socket 22 | mat.node_tree.links.new(from_socket, to_socket) 23 | return 24 | 25 | # create new node or take existing 26 | index_out = socket_index(old_from_socket) 27 | if old_node in new_nodes.keys(): 28 | new_node = new_nodes[old_node] 29 | # link 30 | from_socket = new_node.outputs[index_out] 31 | to_socket = link_to_socket 32 | mat.node_tree.links.new(from_socket, to_socket) 33 | return 34 | else: 35 | new_node = duplicate_node(mat, old_node) 36 | new_nodes[old_node] = new_node 37 | # link 38 | from_socket = new_node.outputs[index_out] 39 | to_socket = link_to_socket 40 | mat.node_tree.links.new(from_socket, to_socket) 41 | 42 | for input_socket in old_node.inputs: 43 | if input_socket.is_linked: 44 | index_in = socket_index(input_socket) 45 | link_to_socket = new_node.inputs[index_in] 46 | duplicate_from_input_socket( 47 | mat, input_socket, link_to_socket) 48 | 49 | for group_node in group_nodes: 50 | 51 | if group_node.type == 'GROUP': 52 | 53 | # group_input_outputs 54 | group_input_nodes = [ 55 | n for n in group_node.node_tree.nodes if n.type == 'GROUP_INPUT'] 56 | output_count = len(group_input_nodes[0].outputs) 57 | group_input_outputs = [None] * output_count 58 | for node in group_input_nodes: 59 | for i, output in enumerate(node.outputs): 60 | if output.is_linked: 61 | group_input_outputs[i] = output 62 | 63 | # group_output_inputs 64 | group_output_nodes = [ 65 | n for n in group_node.node_tree.nodes if n.type == 'GROUP_OUTPUT'] 66 | input_count = len(group_output_nodes[0].inputs) 67 | group_output_inputs = [None] * input_count 68 | for node in group_output_nodes: 69 | for i, input in enumerate(node.inputs): 70 | if input.is_linked: 71 | group_output_inputs[i] = input 72 | 73 | # new value nodes 74 | if do_ungroup_values: 75 | for index, input in enumerate(group_node.inputs): 76 | if group_input_outputs[index]: 77 | if not input.is_linked: 78 | val = input.default_value 79 | tmp_node = None 80 | if input.type == 'VALUE': 81 | tmp_node = mat.node_tree.nodes.new( 82 | type="ShaderNodeValue") 83 | tmp_node.outputs[0].default_value = val 84 | elif input.type == 'RGBA': 85 | tmp_node = mat.node_tree.nodes.new( 86 | type="ShaderNodeRGB") 87 | tmp_node.outputs[0].default_value = val 88 | if tmp_node: 89 | val_nodes.append(tmp_node) 90 | from_socket = tmp_node.outputs[0] 91 | to_socket = input 92 | mat.node_tree.links.new(from_socket, to_socket) 93 | 94 | for output in group_node.outputs: 95 | if output.is_linked: 96 | index = socket_index(output) 97 | input_socket = group_output_inputs[index] 98 | to_sockets = [ 99 | link.to_socket for link in group_node.outputs[index].links] 100 | for link_to_socket in to_sockets: 101 | duplicate_from_input_socket( 102 | mat, input_socket, link_to_socket) 103 | 104 | # delete group node 105 | mat.node_tree.nodes.remove(group_node) 106 | 107 | # remove non linked value nodes 108 | for node in val_nodes: 109 | if not node.outputs[0].is_linked: 110 | mat.node_tree.nodes.remove(node) 111 | val_nodes.remove(node) 112 | 113 | return list(new_nodes.values()) + val_nodes 114 | -------------------------------------------------------------------------------- /nodes/value_list.py: -------------------------------------------------------------------------------- 1 | 2 | def get_value_from_node_by_name(node, value_name): 3 | tmp_value_name = value_name 4 | if value_name == 'Color' and node.type == 'BSDF_PRINCIPLED': 5 | tmp_value_name = "Base Color" 6 | 7 | if tmp_value_name in node.inputs.keys(): 8 | if node.inputs[tmp_value_name].type == 'RGBA': 9 | r, g, b, a = node.inputs[tmp_value_name].default_value 10 | return [r, g, b, a] 11 | else: 12 | return node.inputs[value_name].default_value 13 | -------------------------------------------------------------------------------- /panel.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import Panel 3 | 4 | from .presets import (PBAKER_AddCombinePresetObjectDisplay, 5 | PBAKER_AddPresetObjectDisplay, 6 | PBAKER_AddSuffixPresetObjectDisplay, 7 | PBAKER_MT_display_combine_presets, 8 | PBAKER_MT_display_presets, 9 | PBAKER_MT_display_suffix_presets) 10 | 11 | 12 | class PBAKER_PT_SubPanel(Panel): 13 | bl_space_type = "NODE_EDITOR" 14 | bl_region_type = 'UI' 15 | bl_context = "objectmode" 16 | bl_label = "Subpanel" 17 | 18 | def __init__(self): 19 | self.settings = bpy.context.scene.principled_baker_settings 20 | self.render_settings = bpy.context.scene.render.bake 21 | 22 | def draw(self, context): 23 | pass 24 | 25 | 26 | class PBAKER_PT_BakeList(PBAKER_PT_SubPanel): 27 | bl_parent_id = "PBAKER_PT_Main" 28 | bl_label = "Autodetect/Bake List" 29 | 30 | def draw(self, context): 31 | # Autodetect 32 | col = self.layout.column(align=True) 33 | col.prop(self.settings, "use_autodetect") 34 | col.separator() 35 | 36 | # Bakelist 37 | col_bakelist = col.column() 38 | col_bakelist.label(text="Bake List:") 39 | col_bakelist.template_list("PBAKER_UL_List", "Bake_List", context.scene, 40 | "principled_baker_bakelist", 41 | context.scene, "principled_baker_bakelist_index") 42 | 43 | # Bakelist: Dected and Disable All 44 | row = col_bakelist.row(align=True) 45 | row.operator('principled_baker_bakelist.detect', 46 | text='Detect') 47 | row.separator() 48 | row.operator('principled_baker_bakelist.disable_all', 49 | text='Disable All') 50 | 51 | # Bakelist: Create and Delete 52 | row2 = col_bakelist.row(align=True) 53 | row2_create = row2.row() 54 | row2_create.operator('principled_baker_bakelist.init', text='Create') 55 | row2.separator() 56 | row2_delete = row2.row() 57 | row2_delete.operator('principled_baker_bakelist.delete', text='Delete') 58 | 59 | if len(bpy.context.scene.principled_baker_bakelist) != 0: 60 | row2_create.active = False 61 | 62 | if len(bpy.context.scene.principled_baker_bakelist) == 0: 63 | row2_delete.active = False 64 | 65 | # Bakelist: Up, Down and Short List 66 | row3 = col_bakelist.row(align=True) 67 | row3.operator('principled_baker_bakelist.move_up', 68 | text="", icon='TRIA_UP') 69 | row3.operator('principled_baker_bakelist.move_down', 70 | text="", icon='TRIA_DOWN') 71 | row3.separator() 72 | row3.prop(self.settings, "use_shortlist") 73 | 74 | if len(bpy.context.scene.principled_baker_bakelist) == 0: 75 | row3.active = False 76 | 77 | # Bakelist Presets 78 | row = col_bakelist.row(align=True) 79 | row.menu(PBAKER_MT_display_presets.__name__, 80 | text=PBAKER_MT_display_presets.bl_label) 81 | row.operator(PBAKER_AddPresetObjectDisplay.bl_idname, 82 | text="", icon='ADD') 83 | row.operator(PBAKER_AddPresetObjectDisplay.bl_idname, 84 | text="", icon='REMOVE').remove_active = True 85 | 86 | if self.settings.use_autodetect: 87 | col_bakelist.active = False 88 | 89 | col.separator() 90 | col.label(text="Detection Options:") 91 | col.prop(self.settings, "use_value_differ") 92 | col.prop(self.settings, "use_connected_inputs") 93 | 94 | # col.separator() 95 | # col.label(text="Additional Bake Types:") 96 | 97 | # # Glossiness 98 | # row = col.split() 99 | # row.prop(self.settings, "use_invert_roughness") 100 | 101 | # # Diffuse 102 | # col1 = col.column(align=True) 103 | # row = col1.split() 104 | # row.prop(self.settings, "use_Diffuse") 105 | # if self.settings.individual_samples: 106 | # row.prop(self.settings, "samples_diffuse", text="") 107 | # if self.settings.color_depth == 'INDIVIDUAL': 108 | # row_cd = row.row() 109 | # row_cd.prop(self.settings, "color_depth_diffuse", expand=True) 110 | # row_diff = col.row(align=True) 111 | # if self.settings.use_Diffuse: 112 | # row_diff.prop(self.render_settings, "use_pass_direct", 113 | # text="Direct", toggle=True) 114 | # row_diff.prop(self.render_settings, "use_pass_indirect", 115 | # text="Indirect", toggle=True) 116 | # row_diff.prop(self.render_settings, "use_pass_color", 117 | # text="Color", toggle=True) 118 | # if self.settings.bake_mode == 'SELECTED_TO_ACTIVE': 119 | # col1.active = False 120 | # row_diff.active = False 121 | 122 | # col2 = col.column(align=True) 123 | 124 | # row = col2.split() 125 | # row.prop(self.settings, "use_Bump") 126 | # if self.settings.individual_samples: 127 | # row.prop(self.settings, "samples_bump", text="") 128 | # if self.settings.color_depth == 'INDIVIDUAL': 129 | # row_cd = row.row() 130 | # row_cd.prop(self.settings, "color_depth_bump", expand=True) 131 | 132 | # row = col2.split() 133 | # row.prop(self.settings, "use_vertex_color") 134 | # if self.settings.individual_samples: 135 | # row.prop(self.settings, "samples_vertex_color", text="") 136 | # if self.settings.color_depth == 'INDIVIDUAL': 137 | # row_cd = row.row() 138 | # row_cd.prop(self.settings, "color_depth_vertex_color", expand=True) 139 | # if self.settings.use_vertex_color: 140 | # col2.row().prop(self.settings, "bake_vertex_colors") 141 | 142 | # row = col2.split() 143 | # row.prop(self.settings, "use_material_id") 144 | # if self.settings.individual_samples: 145 | # row.prop(self.settings, "samples_material_id", text="") 146 | # if self.settings.color_depth == 'INDIVIDUAL': 147 | # row_cd = row.row() 148 | # row_cd.prop(self.settings, "color_depth_material_id", expand=True) 149 | 150 | # row = col2.split() 151 | # row.prop(self.settings, "use_wireframe") 152 | # if self.settings.individual_samples: 153 | # row.prop(self.settings, "samples_wireframe", text="") 154 | # if self.settings.color_depth == 'INDIVIDUAL': 155 | # row_cd = row.row() 156 | # row_cd.prop(self.settings, "color_depth_wireframe", expand=True) 157 | 158 | # if self.settings.use_wireframe: 159 | # wf_row = col2.split() 160 | # wf_row.prop(self.settings, "wireframe_size") 161 | # wf_row.prop(self.settings, "use_pixel_size") 162 | 163 | 164 | class PBAKER_PT_AdditionalBakeTypes(PBAKER_PT_SubPanel): 165 | bl_parent_id = "PBAKER_PT_Main" 166 | bl_label = "Additional Bake Types" 167 | bl_options = {'DEFAULT_CLOSED'} 168 | 169 | def draw(self, context): 170 | col = self.layout.column(align=True) 171 | col.label(text="Additional Bake Types:") 172 | 173 | # Glossiness 174 | row = col.split() 175 | row.prop(self.settings, "use_invert_roughness") 176 | 177 | # Diffuse 178 | col1 = col.column(align=True) 179 | row = col1.split() 180 | row.prop(self.settings, "use_Diffuse") 181 | if self.settings.individual_samples: 182 | row.prop(self.settings, "samples_diffuse", text="") 183 | if self.settings.color_depth == 'INDIVIDUAL': 184 | row_cd = row.row() 185 | row_cd.prop(self.settings, "color_depth_diffuse", expand=True) 186 | row_diff = col.row(align=True) 187 | if self.settings.use_Diffuse: 188 | row_diff.prop(self.render_settings, "use_pass_direct", 189 | text="Direct", toggle=True) 190 | row_diff.prop(self.render_settings, "use_pass_indirect", 191 | text="Indirect", toggle=True) 192 | row_diff.prop(self.render_settings, "use_pass_color", 193 | text="Color", toggle=True) 194 | if self.settings.bake_mode == 'SELECTED_TO_ACTIVE': 195 | col1.active = False 196 | row_diff.active = False 197 | 198 | col2 = col.column(align=True) 199 | 200 | row = col2.split() 201 | row.prop(self.settings, "use_Bump") 202 | if self.settings.individual_samples: 203 | row.prop(self.settings, "samples_bump", text="") 204 | if self.settings.color_depth == 'INDIVIDUAL': 205 | row_cd = row.row() 206 | row_cd.prop(self.settings, "color_depth_bump", expand=True) 207 | 208 | row = col2.split() 209 | row.prop(self.settings, "use_vertex_color") 210 | if self.settings.individual_samples: 211 | row.prop(self.settings, "samples_vertex_color", text="") 212 | if self.settings.color_depth == 'INDIVIDUAL': 213 | row_cd = row.row() 214 | row_cd.prop(self.settings, "color_depth_vertex_color", expand=True) 215 | if self.settings.use_vertex_color: 216 | col2.row().prop(self.settings, "bake_vertex_colors") 217 | 218 | row = col2.split() 219 | row.prop(self.settings, "use_material_id") 220 | if self.settings.individual_samples: 221 | row.prop(self.settings, "samples_material_id", text="") 222 | if self.settings.color_depth == 'INDIVIDUAL': 223 | row_cd = row.row() 224 | row_cd.prop(self.settings, "color_depth_material_id", expand=True) 225 | 226 | row = col2.split() 227 | row.prop(self.settings, "use_wireframe") 228 | if self.settings.individual_samples: 229 | row.prop(self.settings, "samples_wireframe", text="") 230 | if self.settings.color_depth == 'INDIVIDUAL': 231 | row_cd = row.row() 232 | row_cd.prop(self.settings, "color_depth_wireframe", expand=True) 233 | 234 | if self.settings.use_wireframe: 235 | wf_row = col2.split() 236 | wf_row.prop(self.settings, "wireframe_size") 237 | wf_row.prop(self.settings, "use_pixel_size") 238 | 239 | 240 | class PBAKER_PT_OutputSettings(PBAKER_PT_SubPanel): 241 | bl_parent_id = "PBAKER_PT_Main" 242 | bl_label = "Output Settings/Bake Settings" 243 | bl_options = {'DEFAULT_CLOSED'} 244 | 245 | def draw(self, context): 246 | # output options: 247 | col = self.layout.column(align=True) 248 | row = col.row() 249 | row.prop(self.settings, "resolution", expand=True) 250 | if self.settings.resolution == 'CUSTOM': 251 | col.prop(self.settings, "custom_resolution") 252 | col.separator() 253 | col.prop(self.settings, "file_path") 254 | col.prop(self.settings, "use_overwrite") 255 | col.prop(self.settings, "use_texture_folder") 256 | 257 | col.separator() 258 | 259 | # image settings: 260 | col.prop(self.settings, "file_format") 261 | 262 | row = col.row() 263 | row.prop(self.settings, "color_mode", text="Color", expand=True) 264 | 265 | if self.settings.color_depth == 'INDIVIDUAL' and self.settings.use_autodetect: 266 | col.label(text="Set Color Depth for Autodetect!", icon='ERROR') 267 | row = col.row() 268 | row.prop(self.settings, "color_depth", text="Color Depth", expand=True) 269 | 270 | if self.settings.file_format == 'PNG': 271 | col.prop(self.settings, "compression", text="Compression") 272 | 273 | if self.settings.file_format == 'OPEN_EXR': 274 | col.prop(self.settings, "exr_codec", text="Codec") 275 | 276 | if self.settings.file_format == 'TIFF': 277 | col.prop(self.settings, "tiff_codec", text="Compression") 278 | 279 | if self.settings.file_format == 'JPEG': 280 | col.prop(self.settings, "quality", text="Quality") 281 | 282 | # Samples 283 | col.separator() 284 | row_samples = col.row() 285 | row_samples.prop(self.settings, "samples") 286 | row_indi_samples = col.row() 287 | row_indi_samples.prop(self.settings, "individual_samples") 288 | if self.settings.individual_samples: 289 | row_samples.active = False 290 | 291 | col.separator() 292 | col.prop(self.render_settings, "margin") 293 | 294 | 295 | class PBAKER_PT_NewMaterial(PBAKER_PT_SubPanel): 296 | bl_parent_id = "PBAKER_PT_Main" 297 | bl_label = "New Material" 298 | bl_options = {'DEFAULT_CLOSED'} 299 | 300 | def draw(self, context): 301 | col = self.layout 302 | col.prop(self.settings, "make_new_material") 303 | col.prop(self.settings, "add_new_material") 304 | col.prop(self.settings, "new_material_prefix") 305 | 306 | 307 | class PBAKER_PT_SelectedToActiveSettings(PBAKER_PT_SubPanel): 308 | bl_parent_id = "PBAKER_PT_Main" 309 | bl_label = "Selected to Active Settings" 310 | bl_options = {'DEFAULT_CLOSED'} 311 | 312 | def draw(self, context): 313 | col2 = self.layout 314 | sub = col2.column() 315 | sub.prop(self.render_settings, "use_cage", text="Cage") 316 | if self.render_settings.use_cage: 317 | sub.prop(self.render_settings, "cage_extrusion", text="Extrusion") 318 | sub.prop(self.render_settings, "cage_object", text="Cage Object") 319 | else: 320 | sub.prop(self.render_settings, 321 | "cage_extrusion", text="Ray Distance") 322 | 323 | if not self.settings.bake_mode == 'SELECTED_TO_ACTIVE': 324 | col2.active = False 325 | 326 | 327 | class PBAKER_PT_PrefixSuffixSettings(PBAKER_PT_SubPanel): 328 | bl_parent_id = "PBAKER_PT_Main" 329 | bl_label = "Prefix/Suffix Settings" 330 | bl_options = {'DEFAULT_CLOSED'} 331 | 332 | def draw(self, context): 333 | 334 | # Prefix 335 | col = self.layout 336 | col.label(text="Prefix Settings:") 337 | col.prop(self.settings, "image_prefix") 338 | col.prop(self.settings, "use_first_material_name") 339 | col.prop(self.settings, "use_object_name") 340 | 341 | if not self.settings.use_object_name: 342 | col.label(text="Possible Overwrites!", icon='ERROR') 343 | 344 | # Suffix 345 | col.label(text="Suffix Settings:") 346 | col = self.layout.column(align=True) 347 | col_suffixlist = col.column() 348 | col_suffixlist.template_list("PBAKER_UL_SuffixList", "Suffix_List", context.scene, 349 | "principled_baker_suffixlist", 350 | context.scene, "principled_baker_suffixlist_index") 351 | 352 | row = col_suffixlist.row(align=True) 353 | init_slist = row.row() 354 | init_slist.operator('principled_baker_suffixlist.init', 355 | text='Create') 356 | if len(bpy.context.scene.principled_baker_suffixlist): 357 | init_slist.active = False 358 | row.separator() 359 | row.operator('principled_baker_suffixlist.reset', 360 | text='Default') 361 | 362 | # Suffix Presets 363 | row = col_suffixlist.row(align=True) 364 | row.menu(PBAKER_MT_display_suffix_presets.__name__, 365 | text=PBAKER_MT_display_suffix_presets.bl_label) 366 | row.operator(PBAKER_AddSuffixPresetObjectDisplay.bl_idname, 367 | text="", icon='ADD') 368 | row.operator(PBAKER_AddSuffixPresetObjectDisplay.bl_idname, 369 | text="", icon='REMOVE').remove_active = True 370 | 371 | # Suffix mods 372 | col.label(text="Suffix String Modifier:") 373 | row = col.row() 374 | row.prop(self.settings, 'suffix_text_mod', expand=True) 375 | 376 | 377 | class PBAKER_PT_AutoSmooth(PBAKER_PT_SubPanel): 378 | bl_parent_id = "PBAKER_PT_Main" 379 | bl_label = "Auto Smooth" 380 | bl_options = {'DEFAULT_CLOSED'} 381 | 382 | def draw(self, context): 383 | self.layout.prop(self.settings, "auto_smooth", 384 | text="Auto Smooth", expand=True) 385 | 386 | 387 | class PBAKER_PT_AutoUVUnwrap(PBAKER_PT_SubPanel): 388 | bl_parent_id = "PBAKER_PT_Main" 389 | bl_label = "Auto UV unwrap" 390 | bl_options = {'DEFAULT_CLOSED'} 391 | 392 | def draw(self, context): 393 | row = self.layout 394 | row.prop(self.settings, "auto_uv_project", 395 | text="Auto UV Project", expand=True) 396 | 397 | if not self.settings.auto_uv_project == 'OFF': 398 | # new UV Map 399 | row.prop(self.settings, "new_uv_map") 400 | if not self.settings.new_uv_map: 401 | self.layout.label( 402 | text="Selected UV Map will be altered!", icon='ERROR') 403 | self.layout.label(text="UV Map settings:") 404 | 405 | if self.settings.auto_uv_project == 'SMART': 406 | col = self.layout 407 | col.prop(self.settings, "angle_limit") 408 | col.prop(self.settings, "island_margin") 409 | col.prop(self.settings, "user_area_weight") 410 | col.prop(self.settings, "use_aspect") 411 | col.prop(self.settings, "stretch_to_bounds") 412 | elif self.settings.auto_uv_project == 'LIGHTMAP': 413 | col = self.layout 414 | col.prop(self.settings, "share_tex_space") 415 | # col.prop(self.settings, "new_uv_map") # see new UV Map 416 | col.prop(self.settings, "new_image") 417 | col.prop(self.settings, "image_size") 418 | col.prop(self.settings, "pack_quality") 419 | col.prop(self.settings, "lightmap_margin") 420 | 421 | 422 | class PBAKER_PT_SelectUVMap(PBAKER_PT_SubPanel): 423 | bl_parent_id = "PBAKER_PT_Main" 424 | bl_label = "Select UV Map" 425 | bl_options = {'DEFAULT_CLOSED'} 426 | 427 | def draw(self, context): 428 | col = self.layout 429 | col.prop(self.settings, "select_uv_map", text="UV Map") 430 | col.prop(self.settings, "set_selected_uv_map") 431 | col.prop(self.settings, "select_set_active_render_uv_map") 432 | 433 | 434 | class PBAKER_PT_CombineChannels(PBAKER_PT_SubPanel): 435 | bl_parent_id = "PBAKER_PT_Main" 436 | bl_label = "Combine Channels" 437 | bl_options = {'DEFAULT_CLOSED'} 438 | 439 | def draw(self, context): 440 | 441 | # Alpha to Color 442 | col = self.layout.column() 443 | col.prop(self.settings, "use_alpha_to_color") 444 | 445 | col.label(text="Custom Combined Images:") 446 | 447 | row = self.layout.row() 448 | row.template_list("PBAKER_UL_CombineList", "Combine_List", context.scene, 449 | "principled_baker_combinelist", 450 | context.scene, "principled_baker_combinelist_index") 451 | 452 | col = row.column(align=True) 453 | col.operator("principled_baker_combinelist.add", icon='ADD', text="") 454 | col.operator("principled_baker_combinelist.delete", 455 | icon='REMOVE', text="") 456 | col.separator() 457 | col.operator("principled_baker_combinelist.move_up", 458 | icon='TRIA_UP', text="") 459 | col.operator("principled_baker_combinelist.move_down", 460 | icon='TRIA_DOWN', text="") 461 | 462 | col = self.layout.column() 463 | col.template_list("PBAKER_UL_CombineList", "Combine_List", context.scene, 464 | "principled_baker_combinelist", 465 | context.scene, "principled_baker_combinelist_index", 466 | type='COMPACT') 467 | 468 | # Combine Presets 469 | row = col.row(align=True) 470 | row.menu(PBAKER_MT_display_combine_presets.__name__, 471 | text=PBAKER_MT_display_combine_presets.bl_label) 472 | row.operator(PBAKER_AddCombinePresetObjectDisplay.bl_idname, 473 | text="", icon='ADD') 474 | row.operator(PBAKER_AddCombinePresetObjectDisplay.bl_idname, 475 | text="", icon='REMOVE').remove_active = True 476 | 477 | 478 | class PBAKER_PT_DuplicateObjects(PBAKER_PT_SubPanel): 479 | bl_parent_id = "PBAKER_PT_Main" 480 | bl_label = "Duplicate Objects" 481 | bl_options = {'DEFAULT_CLOSED'} 482 | 483 | def draw(self, context): 484 | col = self.layout 485 | col.prop(self.settings, "duplicate_objects") 486 | col2 = col.column(align=True) 487 | col2.prop(self.settings, "join_duplicate_objects") 488 | col2.prop(self.settings, "copy_modifiers") 489 | col2.prop(self.settings, "duplicate_objects_prefix") 490 | col2.prop(self.settings, "duplicate_objects_suffix") 491 | col3 = col.column(align=True) 492 | col3.prop(self.settings, "duplicate_object_loc_offset_x") 493 | col3.prop(self.settings, "duplicate_object_loc_offset_y") 494 | col3.prop(self.settings, "duplicate_object_loc_offset_z") 495 | 496 | if not self.settings.duplicate_objects: 497 | col2.active = False 498 | col3.active = False 499 | 500 | if self.settings.bake_mode == 'SELECTED_TO_ACTIVE': 501 | col.active = False 502 | 503 | 504 | class PBAKER_PT_Misc(PBAKER_PT_SubPanel): 505 | bl_parent_id = "PBAKER_PT_Main" 506 | bl_label = "Misc Settings" 507 | bl_options = {'DEFAULT_CLOSED'} 508 | 509 | def draw(self, context): 510 | self.layout.prop(self.settings, "use_exclude_transparent_colors") 511 | 512 | 513 | class PBAKER_PT_Main(Panel): 514 | bl_space_type = "NODE_EDITOR" 515 | bl_region_type = "UI" 516 | bl_label = "Principled Baker" 517 | bl_context = "objectmode" 518 | bl_category = "Principled Baker" 519 | 520 | @classmethod 521 | def poll(cls, context): 522 | if context.space_data.tree_type == 'ShaderNodeTree': 523 | return True 524 | return False 525 | 526 | def draw(self, context): 527 | self.settings = context.scene.principled_baker_settings 528 | 529 | prefs = context.preferences.addons[__package__].preferences 530 | 531 | layout = self.layout 532 | 533 | can_bake = True 534 | 535 | if not bpy.context.scene.render.engine == 'CYCLES' and not prefs.switch_to_cycles: 536 | layout.label(text="Set Render engine to Cycles!", icon='INFO') 537 | can_bake = False 538 | 539 | if self.settings.use_autodetect and self.settings.color_depth == 'INDIVIDUAL': 540 | layout.label(text="Set Color Depth for Autodetect!", icon='INFO') 541 | can_bake = False 542 | 543 | if can_bake: 544 | layout.operator('object.principled_baker_bake', 545 | text='Bake', icon='RENDER_STILL') 546 | 547 | # bake mode 548 | layout.prop(self.settings, "bake_mode", 549 | text="Bake Mode", expand=True) 550 | -------------------------------------------------------------------------------- /prefs.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import BoolProperty, EnumProperty, FloatProperty, StringProperty 3 | 4 | from addon_utils import check 5 | 6 | 7 | # Addon prefs 8 | class PBAKER_prefs(bpy.types.AddonPreferences): 9 | bl_idname = __package__ 10 | 11 | switch_to_cycles: BoolProperty( 12 | name="Bake in Eevee/Workbench (temporarily switch to Cycles)", 13 | default=False 14 | ) 15 | 16 | mat_id_algorithm: EnumProperty( 17 | name="Material ID Colors by", 18 | items=( 19 | ('HUE', 'Slot/Hue', ''), 20 | ('NAME', 'Material Name', ''), 21 | ), 22 | default='HUE' 23 | ) 24 | 25 | mat_id_saturation: FloatProperty( 26 | name="Saturation", 27 | default=1.0, 28 | min=0.0, 29 | max=1.0 30 | ) 31 | 32 | mat_id_value: FloatProperty( 33 | name="Value", 34 | default=1.0, 35 | min=0.0, 36 | max=1.0 37 | ) 38 | 39 | # use_node_wrangler : BoolProperty( 40 | # name="Node Wrangler for Texture Setup", 41 | # default=False 42 | # ) 43 | 44 | def draw(self, context): 45 | layout = self.layout 46 | col = layout.column() 47 | 48 | col.prop(self, "switch_to_cycles") 49 | col.label(text="This may crash Blender!", icon='ERROR') 50 | col.separator() 51 | 52 | col.prop(self, "mat_id_algorithm") 53 | if self.mat_id_algorithm == 'HUE': 54 | col.prop(self, "mat_id_saturation") 55 | col.prop(self, "mat_id_value") 56 | elif self.mat_id_algorithm == 'NAME': 57 | self.layout.label( 58 | text="Duplicate colors are possible!", icon='ERROR') 59 | 60 | # # Node Wrangler for Texture Setup 61 | # if check("node_wrangler"): 62 | # col.prop(self, "use_node_wrangler") 63 | -------------------------------------------------------------------------------- /prepare/material.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..const import ALPHA_NODES, NODE_TAG, NORMAL_INPUTS 4 | from ..nodes.duplicate import duplicate_nodes 5 | from ..nodes.new import new_mixrgb_node, new_rgb_node 6 | from ..nodes.node import (get_all_nodes_linked_from, 7 | is_mixnode_in_node_tree, 8 | is_node_type_in_node_tree) 9 | from ..nodes.outputs import get_active_output 10 | from ..nodes.ungroup import ungroup_nodes 11 | 12 | 13 | def prepare_bake_factor(mat, socket, new_socket, node_type, factor_name='Fac'): 14 | node = socket.node 15 | if node.type == node_type: 16 | to_node = node.outputs[0].links[0].to_node 17 | if factor_name in to_node.inputs.keys(): 18 | socket = to_node.inputs[factor_name] 19 | prepare_bake(mat, socket, new_socket, factor_name) 20 | else: 21 | for input_socket in node.inputs: 22 | if input_socket.is_linked: 23 | from_socket = input_socket.links[0].from_socket 24 | prepare_bake_factor( 25 | mat, from_socket, new_socket, node_type, factor_name) 26 | 27 | 28 | def prepare_bake_ao(mat, socket, new_socket): 29 | 30 | node = socket.node 31 | 32 | if node.type == 'MIX_RGB': 33 | if node.inputs[1].is_linked and node.inputs[2].is_linked: 34 | from_node_1 = node.inputs[1].links[0].from_node 35 | from_node_2 = node.inputs[2].links[0].from_node 36 | is_ao_in_1 = is_node_type_in_node_tree(mat, 37 | from_node_1, 'AMBIENT_OCCLUSION') 38 | is_ao_in_2 = is_node_type_in_node_tree(mat, 39 | from_node_2, 'AMBIENT_OCCLUSION') 40 | if is_ao_in_1 and is_ao_in_2: 41 | from_socket = socket 42 | mat.node_tree.links.new(from_socket, new_socket) 43 | return 44 | 45 | if is_node_type_in_node_tree(mat, node, 'AMBIENT_OCCLUSION'): 46 | if not is_mixnode_in_node_tree(node): 47 | if not socket.type == 'SHADER': 48 | from_socket = socket 49 | mat.node_tree.links.new(from_socket, new_socket) 50 | else: 51 | for input_socket in node.inputs: 52 | if input_socket.is_linked: 53 | from_socket = input_socket.links[0].from_socket 54 | prepare_bake_ao(mat, from_socket, new_socket) 55 | else: 56 | for input_socket in node.inputs: 57 | if input_socket.is_linked: 58 | from_socket = input_socket.links[0].from_socket 59 | prepare_bake_ao(mat, from_socket, new_socket) 60 | else: 61 | for input_socket in node.inputs: 62 | if input_socket.is_linked: 63 | from_socket = input_socket.links[0].from_socket 64 | prepare_bake_ao(mat, from_socket, new_socket) 65 | 66 | 67 | def prepare_bake_color(mat, from_socket, new_socket): 68 | node = from_socket.node 69 | 70 | # find and unlink AO trees in tagged nodes 71 | for node in mat.node_tree.nodes: 72 | if node.type == 'MIX_RGB' and NODE_TAG in node.keys(): 73 | for i, node_input in enumerate(node.inputs[1:]): 74 | if node_input.is_linked: 75 | from_node = node_input.links[0].from_node 76 | if not is_mixnode_in_node_tree(from_node): 77 | if is_node_type_in_node_tree(mat, from_node, 'AMBIENT_OCCLUSION'): 78 | mat.node_tree.links.remove(node_input.links[0]) 79 | 80 | # if 'Fac' not linked, set to 0 or 1 81 | fac_in = node.inputs[0] 82 | if not fac_in.is_linked: 83 | fac_in.default_value = 0 if i == 1 else 1 84 | 85 | mat.node_tree.links.new(from_socket, new_socket) 86 | 87 | 88 | def prepare_bake(mat, socket, new_socket, input_socket_name): 89 | settings = bpy.context.scene.principled_baker_settings 90 | 91 | if input_socket_name in NORMAL_INPUTS: 92 | color = (0.5, 0.5, 1.0, 1.0) 93 | else: 94 | color = (0, 0, 0, 0) 95 | 96 | node = socket.node 97 | 98 | if node.type == 'OUTPUT_MATERIAL': 99 | from_socket = socket.links[0].from_socket 100 | prepare_bake(mat, from_socket, new_socket, input_socket_name) 101 | 102 | elif node.type == 'MIX_SHADER': 103 | color2 = [1, 1, 1, 0] if input_socket_name == 'Fac' else color 104 | fac = node.inputs['Fac'].default_value 105 | mix_node = new_mixrgb_node(mat, fac, color, color2) 106 | mat.node_tree.links.new(mix_node.outputs[0], new_socket) 107 | mix_node.label = input_socket_name 108 | 109 | if node.inputs['Fac'].is_linked: 110 | from_socket = node.inputs[0].links[0].from_socket 111 | new_socket = mix_node.inputs[0] 112 | mat.node_tree.links.new(from_socket, new_socket) 113 | 114 | for i in range(1, 3): 115 | if node.inputs[i].is_linked: 116 | next_node = node.inputs[i].links[0].from_node 117 | if settings.use_exclude_transparent_colors: 118 | if next_node.type in ALPHA_NODES.values() or next_node.type == 'BSDF_TRANSPARENT': 119 | other_i = i % 2 + 1 120 | mix_node.inputs[i].default_value = (0, 0, 0, 0) 121 | mix_node.inputs[other_i].default_value = (1, 1, 1, 0) 122 | if node.inputs[other_i].is_linked: 123 | from_socket = node.inputs[other_i].links[0].from_socket 124 | new_socket = mix_node.inputs[i] 125 | else: 126 | from_socket = node.inputs[i].links[0].from_socket 127 | new_socket = mix_node.inputs[i] 128 | prepare_bake(mat, from_socket, new_socket, 129 | input_socket_name) 130 | 131 | elif node.type == 'ADD_SHADER' and not input_socket_name == 'Fac': 132 | mix_node = new_mixrgb_node(mat, 1, color, color) 133 | mix_node.blend_type = 'ADD' 134 | mat.node_tree.links.new(mix_node.outputs[0], new_socket) 135 | mix_node.label = input_socket_name 136 | 137 | for i, input in enumerate(node.inputs): 138 | if input.is_linked: 139 | from_socket = input.links[0].from_socket 140 | new_socket = mix_node.inputs[i + 1] 141 | prepare_bake(mat, from_socket, new_socket, input_socket_name) 142 | 143 | # exclude some colors from color 144 | elif node.type in {'EMISSION'}: 145 | return 146 | else: 147 | if node.type == 'BSDF_PRINCIPLED' and input_socket_name == 'Color': 148 | input_socket_name = 'Base Color' 149 | 150 | if input_socket_name == 'Ambient Occlusion': 151 | # AO: remove all non-ao branches 152 | for tmp_node in mat.node_tree.nodes: 153 | if tmp_node.type == 'MIX_RGB' and NODE_TAG in tmp_node.keys(): 154 | for i, node_input in enumerate(tmp_node.inputs[1:]): 155 | if node_input.is_linked: 156 | from_node = node_input.links[0].from_node 157 | if not is_node_type_in_node_tree(mat, from_node, 'AMBIENT_OCCLUSION'): 158 | mat.node_tree.links.remove(node_input.links[0]) 159 | 160 | # if 'Fac' not linked, set to 0 or 1 161 | fac_in = tmp_node.inputs[0] 162 | if not fac_in.is_linked: 163 | fac_in.default_value = 0 if i == 1 else 1 164 | 165 | # if Colors not linked, set to white 166 | white = [1, 1, 1, 1] 167 | col1_in = tmp_node.inputs[1] 168 | col2_in = tmp_node.inputs[2] 169 | if not col1_in.is_linked: 170 | col1_in.default_value = white 171 | if not col2_in.is_linked: 172 | col2_in.default_value = white 173 | 174 | # AO: link ao branch 175 | for input_socket in node.inputs: 176 | if input_socket.type == 'RGBA': 177 | if input_socket.is_linked: 178 | from_node = input_socket.links[0].from_node 179 | if is_node_type_in_node_tree(mat, from_node, 'AMBIENT_OCCLUSION'): 180 | from_socket = input_socket.links[0].from_socket 181 | mat.node_tree.links.new(from_socket, new_socket) 182 | 183 | elif input_socket_name in node.inputs.keys(): 184 | input_socket = node.inputs[input_socket_name] 185 | 186 | if input_socket.type == 'RGBA': 187 | if input_socket.is_linked: 188 | o = input_socket.links[0].from_socket 189 | prepare_bake_color(mat, o, new_socket) 190 | else: 191 | color = node.inputs[input_socket_name].default_value 192 | rgb_node = new_rgb_node(mat, color) 193 | mat.node_tree.links.new(rgb_node.outputs[0], new_socket) 194 | 195 | elif input_socket.type == 'VALUE': 196 | if input_socket.is_linked: 197 | from_socket = input_socket.links[0].from_socket 198 | if from_socket.type == 'VALUE': 199 | mat.node_tree.links.new(from_socket, new_socket) 200 | else: # RGB to BW 201 | node = mat.node_tree.nodes.new( 202 | type="ShaderNodeRGBToBW") 203 | node[NODE_TAG] = 1 204 | mat.node_tree.links.new(from_socket, node.inputs[0]) 205 | mat.node_tree.links.new(node.outputs[0], new_socket) 206 | else: 207 | value_node = mat.node_tree.nodes.new( 208 | type="ShaderNodeValue") 209 | value_node[NODE_TAG] = 1 210 | value_node.outputs[0].default_value = node.inputs[input_socket_name].default_value 211 | mat.node_tree.links.new(value_node.outputs[0], new_socket) 212 | 213 | elif input_socket.type == 'VECTOR': 214 | if input_socket.name == input_socket_name: 215 | if input_socket.is_linked: 216 | from_socket = input_socket.links[0].from_socket 217 | mat.node_tree.links.new(from_socket, new_socket) 218 | 219 | else: 220 | for input_socket in node.inputs: 221 | if input_socket.is_linked: 222 | from_socket = input_socket.links[0].from_socket 223 | prepare_bake(mat, from_socket, new_socket, 224 | input_socket_name) 225 | 226 | 227 | def prepare_material_for_bake(mat, do_ungroup_values=True): 228 | 229 | # Duplicate node tree from active output 230 | active_output = get_active_output(mat) 231 | selected_nodes = get_all_nodes_linked_from(active_output) 232 | selected_nodes = duplicate_nodes(mat, selected_nodes, keep_inputs=True) 233 | 234 | # TAG all selected nodes for clean up 235 | for node in selected_nodes: 236 | node[NODE_TAG] = 1 237 | 238 | # Ungroup all groups in selected nodes 239 | group_nodes = [n for n in selected_nodes if n.type == 'GROUP'] 240 | selected_nodes = ungroup_nodes( 241 | mat, group_nodes, do_ungroup_values=do_ungroup_values) 242 | 243 | # TAG all selected nodes for clean up 244 | for node in selected_nodes: 245 | node[NODE_TAG] = 1 246 | 247 | # put temp nodes in frame 248 | p_baker_frame = mat.node_tree.nodes.new(type="NodeFrame") 249 | for node in mat.node_tree.nodes: 250 | if NODE_TAG in node.keys(): 251 | node.parent = p_baker_frame 252 | p_baker_frame.name = "p_baker_temp_frame" 253 | p_baker_frame.label = "PRINCIPLED BAKER NODES (If you see this, something went wrong!)" 254 | p_baker_frame.use_custom_color = True 255 | p_baker_frame.color = (1, 0, 0) 256 | p_baker_frame[NODE_TAG] = 1 257 | p_baker_frame.label_size = 64 258 | 259 | new_output = None 260 | for node in mat.node_tree.nodes: 261 | if NODE_TAG in node.keys() and node.type == "OUTPUT_MATERIAL": 262 | new_output = node 263 | new_output.is_active_output = True 264 | return new_output 265 | -------------------------------------------------------------------------------- /prepare/objects.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import bpy 4 | 5 | from ..const import ALPHA_NODES, NODE_TAG, PB_PACKAGE 6 | from ..functions import get_bake_type_by 7 | from ..nodes.new import new_pb_emission_node, new_pb_output_node 8 | from ..nodes.node import is_node_type_in_node_tree 9 | from ..nodes.outputs import (deactivate_material_outputs, 10 | get_active_output) 11 | from ..prepare.material import (prepare_bake, 12 | prepare_bake_factor, 13 | prepare_material_for_bake) 14 | 15 | 16 | def prepare_objects_for_bake_matid(objs): 17 | 18 | def create_temp_nodes(mat, color): 19 | pb_output_node = new_pb_output_node(mat) 20 | pb_emission_node = new_pb_emission_node(mat, color) 21 | 22 | # activate temp output 23 | material_output = get_active_output(mat) # orig mat output 24 | if material_output: 25 | material_output.is_active_output = False 26 | pb_output_node.is_active_output = True 27 | 28 | # link pb_emission_node to material_output 29 | mat.node_tree.links.new(pb_emission_node.outputs[0], 30 | pb_output_node.inputs['Surface']) 31 | 32 | if not isinstance(objs, list): 33 | objects = [objects] 34 | 35 | materials = [] 36 | for obj in objs: 37 | for mat_slot in obj.material_slots: 38 | if mat_slot.material: 39 | if mat_slot.material not in materials: 40 | materials.append(mat_slot.material) 41 | 42 | prefs = bpy.context.preferences.addons[PB_PACKAGE].preferences 43 | if prefs.mat_id_algorithm == 'HUE': 44 | from mathutils import Color 45 | n_materials = len(materials) 46 | s = prefs.mat_id_saturation 47 | v = prefs.mat_id_value 48 | for mat_index, mat in enumerate(materials): 49 | h = mat_index / n_materials 50 | c = Color([0, 0, 0]) 51 | c.hsv = h, s, v 52 | color = c.r, c.g, c.b, 1.0 53 | create_temp_nodes(mat, color) 54 | 55 | elif prefs.mat_id_algorithm == 'NAME': 56 | from hashlib import sha1 57 | for mat in materials: 58 | s = mat.name.encode('utf-8') 59 | h = int(sha1(s).hexdigest(), base=16) 60 | r = h % 256 / 256 61 | g = (h >> 32) % 256 / 256 62 | b = (h >> 16) % 256 / 256 63 | color = r, g, b, 1.0 64 | create_temp_nodes(mat, color) 65 | 66 | 67 | def prepare_objects_for_bake_vertex_color(objs, vertex_color_name): 68 | if not isinstance(objs, list): 69 | objects = [objects] 70 | 71 | for obj in objs: 72 | for mat_slot in obj.material_slots: 73 | if mat_slot.material: 74 | mat = mat_slot.material 75 | 76 | pb_output_node = new_pb_output_node(mat) 77 | pb_emission_node = new_pb_emission_node(mat) 78 | socket_to_pb_emission_node_color = pb_emission_node.inputs['Color'] 79 | 80 | # activate temp output 81 | material_output = get_active_output(mat) 82 | if material_output: 83 | material_output.is_active_output = False 84 | pb_output_node.is_active_output = True 85 | 86 | attr_node = mat.node_tree.nodes.new( 87 | type='ShaderNodeAttribute') 88 | attr_node[NODE_TAG] = 1 # tag for clean up 89 | # attr_node.attribute_name = active_vert_col 90 | attr_node.attribute_name = vertex_color_name 91 | mat.node_tree.links.new(attr_node.outputs['Color'], 92 | socket_to_pb_emission_node_color) 93 | 94 | # link pb_emission_node to material_output 95 | mat.node_tree.links.new(pb_emission_node.outputs[0], 96 | pb_output_node.inputs['Surface']) 97 | 98 | 99 | def prepare_objects_for_bake_wireframe(objs): 100 | if not isinstance(objs, list): 101 | objects = [objects] 102 | 103 | for obj in objs: 104 | for mat_slot in obj.material_slots: 105 | if mat_slot.material: 106 | mat = mat_slot.material 107 | 108 | pb_output_node = new_pb_output_node(mat) 109 | pb_emission_node = new_pb_emission_node(mat) 110 | socket_to_pb_emission_node_color = pb_emission_node.inputs['Color'] 111 | 112 | # activate temp output 113 | material_output = get_active_output(mat) 114 | if material_output: 115 | material_output.is_active_output = False 116 | pb_output_node.is_active_output = True 117 | 118 | wf_node = mat.node_tree.nodes.new(type='ShaderNodeWireframe') 119 | wf_node[NODE_TAG] = 1 # tag for clean up 120 | settings = bpy.context.scene.principled_baker_settings 121 | wf_node.inputs[0].default_value = settings.wireframe_size 122 | wf_node.use_pixel_size = settings.use_pixel_size 123 | mat.node_tree.links.new(wf_node.outputs[0], 124 | socket_to_pb_emission_node_color) 125 | 126 | # link pb_emission_node to material_output 127 | mat.node_tree.links.new(pb_emission_node.outputs[0], 128 | pb_output_node.inputs['Surface']) 129 | 130 | 131 | def prepare_objects_for_bake(objs, jobname): 132 | 133 | def prepare_material(mat): 134 | 135 | # skip already prepared material 136 | for node in mat.node_tree.nodes: 137 | if NODE_TAG in node.keys(): 138 | return 139 | 140 | active_output = prepare_material_for_bake(mat) 141 | 142 | # Deselect all nodes 143 | for node in mat.node_tree.nodes: 144 | node.select = False 145 | 146 | # temp nodes 147 | for node in mat.node_tree.nodes: 148 | if node.type == "OUTPUT_MATERIAL" and NODE_TAG in node.keys(): 149 | material_output = node 150 | pb_output_node = new_pb_output_node(mat) 151 | pb_emission_node = new_pb_emission_node(mat, [1, 1, 1, 1]) 152 | pb_output_node.location.x = active_output.location.x 153 | pb_emission_node.location.x = active_output.location.x 154 | 155 | socket_to_pb_emission_node_color = pb_emission_node.inputs['Color'] 156 | 157 | # activate temp output and deactivate others 158 | deactivate_material_outputs(mat) 159 | pb_output_node.is_active_output = True 160 | 161 | socket_to_surface = material_output.inputs['Surface'].links[0].from_socket 162 | bake_type = get_bake_type_by(jobname) 163 | 164 | if bake_type == 'EMIT': 165 | if jobname in ALPHA_NODES.keys(): 166 | prepare_bake_factor( 167 | mat, socket_to_surface, socket_to_pb_emission_node_color, ALPHA_NODES[jobname], 'Fac') 168 | 169 | elif jobname == 'Alpha': 170 | if is_node_type_in_node_tree(mat, material_output, 'BSDF_TRANSPARENT'): 171 | prepare_bake_factor( 172 | mat, socket_to_surface, socket_to_pb_emission_node_color, 'BSDF_TRANSPARENT', 'Fac') 173 | else: 174 | prepare_bake(mat, socket_to_surface, 175 | socket_to_pb_emission_node_color, 'Alpha') 176 | 177 | elif jobname == 'Displacement': 178 | if material_output.inputs['Displacement'].is_linked: 179 | socket_to_displacement = material_output.inputs[ 180 | 'Displacement'].links[0].from_socket 181 | node = material_output.inputs['Displacement'].links[0].from_node 182 | if node.type == 'DISPLACEMENT': 183 | prepare_bake(mat, socket_to_displacement, 184 | socket_to_pb_emission_node_color, 'Height') 185 | else: 186 | from_socket = socket_to_displacement.links[0].from_socket 187 | mat.node_tree.links.new(from_socket, 188 | socket_to_pb_emission_node_color) 189 | 190 | elif jobname == 'Bump': 191 | prepare_bake(mat, socket_to_surface, 192 | socket_to_pb_emission_node_color, 'Height') 193 | elif jobname == 'Ambient Occlusion': 194 | prepare_bake(mat, socket_to_surface, 195 | socket_to_pb_emission_node_color, 'Ambient Occlusion') 196 | else: 197 | prepare_bake(mat, socket_to_surface, 198 | socket_to_pb_emission_node_color, jobname) 199 | 200 | # link pb_emission_node to material_output 201 | mat.node_tree.links.new(pb_emission_node.outputs[0], 202 | pb_output_node.inputs['Surface']) 203 | 204 | # put temp nodes in a frame 205 | p_baker_frame = mat.node_tree.nodes["p_baker_temp_frame"] 206 | for node in mat.node_tree.nodes: 207 | if NODE_TAG in node.keys(): 208 | node.parent = p_baker_frame 209 | 210 | if not isinstance(objs, list): 211 | objs = [objs] 212 | 213 | for obj in objs: 214 | for mat_slot in obj.material_slots: 215 | if mat_slot.material: 216 | mat = mat_slot.material 217 | if jobname not in {"Emission", "Normal"}: 218 | prepare_material(mat) 219 | -------------------------------------------------------------------------------- /presets.py: -------------------------------------------------------------------------------- 1 | from bpy.types import Menu, Operator 2 | 3 | from bl_operators.presets import AddPresetBase 4 | 5 | PRESET_BAKELIST_SUBDIR = "principled_baker/bake_list" 6 | PRESET_SUFFIXLIST_SUBDIR = "principled_baker/suffix_list" 7 | PRESET_COMBINELIST_SUBDIR = "principled_baker/combine_list" 8 | 9 | 10 | # -------------------------------------------------------- 11 | # Bake List Presets 12 | # -------------------------------------------------------- 13 | class PBAKER_MT_display_presets(Menu): 14 | bl_label = "Bake List Presets" 15 | preset_subdir = PRESET_BAKELIST_SUBDIR 16 | preset_operator = "script.execute_preset" 17 | draw = Menu.draw_preset 18 | 19 | 20 | class PBAKER_AddPresetObjectDisplay(AddPresetBase, Operator): 21 | bl_idname = "principled_baker.preset_add" 22 | bl_label = "Add Bake List Preset" 23 | preset_menu = "PBAKER_MT_display_presets" 24 | 25 | preset_defines = ["scene = bpy.context.scene"] 26 | preset_values = ["scene.principled_baker_bakelist"] 27 | preset_subdir = PRESET_BAKELIST_SUBDIR 28 | 29 | 30 | # -------------------------------------------------------- 31 | # Suffix Presets 32 | # -------------------------------------------------------- 33 | class PBAKER_MT_display_suffix_presets(Menu): 34 | bl_label = "Bake List Presets" 35 | preset_subdir = PRESET_SUFFIXLIST_SUBDIR 36 | preset_operator = "script.execute_preset" 37 | draw = Menu.draw_preset 38 | 39 | 40 | class PBAKER_AddSuffixPresetObjectDisplay(AddPresetBase, Operator): 41 | bl_idname = "principled_baker.suffix_preset_add" 42 | bl_label = "Add Suffix List Preset" 43 | preset_menu = "PBAKER_MT_display_suffix_presets" 44 | 45 | preset_defines = ["scene = bpy.context.scene"] 46 | preset_values = ["scene.principled_baker_suffixlist"] 47 | preset_subdir = PRESET_SUFFIXLIST_SUBDIR 48 | 49 | 50 | # -------------------------------------------------------- 51 | # Combine Presets 52 | # -------------------------------------------------------- 53 | class PBAKER_MT_display_combine_presets(Menu): 54 | bl_label = "Combine List Presets" 55 | preset_subdir = PRESET_COMBINELIST_SUBDIR 56 | preset_operator = "script.execute_preset" 57 | draw = Menu.draw_preset 58 | 59 | 60 | class PBAKER_AddCombinePresetObjectDisplay(AddPresetBase, Operator): 61 | bl_idname = "principled_baker.combine_preset_add" 62 | bl_label = "Add Combine List Preset" 63 | preset_menu = "PBAKER_MT_display_combine_presets" 64 | 65 | preset_defines = ["scene = bpy.context.scene"] 66 | preset_values = ["scene.principled_baker_combinelist"] 67 | preset_subdir = PRESET_COMBINELIST_SUBDIR 68 | -------------------------------------------------------------------------------- /set_samples.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def set_samples(jobname): 5 | """Set samples by user settings. 6 | 7 | Must be restored afer baking! 8 | """ 9 | 10 | settings = bpy.context.scene.principled_baker_settings 11 | samples = settings.samples 12 | if settings.individual_samples and not settings.use_autodetect: 13 | bakelist = bpy.context.scene.principled_baker_bakelist 14 | if jobname in bakelist.keys(): 15 | samples = bakelist[jobname].samples 16 | else: 17 | if jobname == "Diffuse": 18 | samples = settings.samples_diffuse 19 | elif jobname == "Bump": 20 | samples = settings.samples_bump 21 | elif jobname == "Vertex Color": 22 | samples = settings.samples_vertex_color 23 | elif jobname == "Material ID": 24 | samples = settings.samples_material_id 25 | elif jobname == "Wireframe": 26 | samples = settings.samples_wireframe 27 | bpy.context.scene.cycles.samples = samples 28 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import (BoolProperty, EnumProperty, FloatProperty, IntProperty, 3 | StringProperty) 4 | from bpy.types import PropertyGroup 5 | 6 | 7 | def color_mode_items(scene, context): 8 | if scene.file_format in ['PNG', 'TARGA', 'TIFF', 'OPEN_EXR']: 9 | items = [ 10 | ('RGB', "RGB", ""), 11 | ('RGBA', "RGBA", ""), 12 | # ('BW', "BW", ""), # TODO reenable and auto set to RGB/RGBA, if needed 13 | ] 14 | else: 15 | items = [ 16 | ('BW', "BW", ""), 17 | ('RGB', "RGB", "") 18 | ] 19 | return items 20 | 21 | 22 | def color_depth_individual_items(scene, context): 23 | if scene.file_format == 'OPEN_EXR': 24 | items = [ 25 | ('16', "Float (Half)", ""), 26 | ('32', "Float (Full)", ""), 27 | ('INDIVIDUAL', "Individual", ""), 28 | ] 29 | else: 30 | items = [ 31 | ('8', "8", ""), 32 | ('16', "16", ""), 33 | ('INDIVIDUAL', "Individual", ""), 34 | ] 35 | return items 36 | 37 | 38 | def color_depth_items(scene, context): 39 | if scene.file_format == 'OPEN_EXR': 40 | items = [ 41 | ('16', "Float (Half)", ""), 42 | ('32', "Float (Full)", ""), 43 | ] 44 | else: 45 | items = [ 46 | ('8', "8", ""), 47 | ('16', "16", ""), 48 | ] 49 | return items 50 | 51 | 52 | def reset_bake_list(context, value): 53 | bpy.ops.principled_baker_bakelist.update() 54 | 55 | 56 | class PBAKER_settings(PropertyGroup): 57 | 58 | file_format: EnumProperty( 59 | name="File Format", 60 | items=( 61 | ('PNG', 'PNG', ''), 62 | ('BMP', 'BMP', ''), 63 | ('JPEG', 'JPEG', ''), 64 | ('TIFF', 'TIFF', ''), 65 | ('TARGA', 'Targa', ''), 66 | ('OPEN_EXR', 'OpenEXR', ''), 67 | ), 68 | default='PNG' 69 | ) 70 | 71 | color_mode: EnumProperty( 72 | name="Color", 73 | items=color_mode_items 74 | ) 75 | 76 | color_depth: EnumProperty( 77 | name="Color Depth", 78 | items=color_depth_individual_items 79 | ) 80 | 81 | compression: IntProperty( 82 | name="Quality", 83 | default=15, 84 | min=0, 85 | soft_max=100, 86 | step=1, 87 | subtype='PERCENTAGE' 88 | ) 89 | 90 | exr_codec: EnumProperty( 91 | name="Codec", 92 | items=( 93 | ('NONE', 'None', ''), 94 | ('PXR24', 'Pxr24 (lossy)', ''), 95 | ('ZIP', 'ZIP (lossless)', ''), 96 | ('PIZ', 'PIZ (lossless)', ''), 97 | ('RLE', 'RLE (lossless)', ''), 98 | ('ZIPS', 'ZIPS (lossless)', ''), 99 | ('DWAA', 'DWAA (lossy)', ''), 100 | ), 101 | default='ZIP' 102 | ) 103 | 104 | tiff_codec: EnumProperty( 105 | name="Compression", 106 | items=( 107 | ('NONE', 'None', ''), 108 | ('DEFLATE', 'Deflate', ''), 109 | ('LZW', 'LZW', ''), 110 | ('PACKBITS', 'Packbits', '') 111 | ), 112 | default='DEFLATE' 113 | ) 114 | 115 | quality: IntProperty( 116 | name="Quality", 117 | default=90, 118 | min=0, 119 | soft_max=100, 120 | step=1, 121 | subtype='PERCENTAGE' 122 | ) 123 | 124 | use_autodetect: BoolProperty( 125 | name="Autodetect", 126 | description="Bake linked inputs and/or inputs with values that differ in different Shader nodes.\n\nThis depends on the selected Detection Options", 127 | default=True 128 | ) 129 | 130 | image_prefix: StringProperty( 131 | name="Prefix", 132 | maxlen=1024, 133 | ) 134 | 135 | use_object_name: BoolProperty( 136 | name="Object Name", 137 | description="Use Object Name as part of Texture Name to get unique names", 138 | default=True 139 | ) 140 | 141 | use_first_material_name: BoolProperty( 142 | name="First Material Name as (second) Prefix", 143 | description="Use first material name as prefix.", 144 | default=False 145 | ) 146 | 147 | image_suffix_settings_show: BoolProperty( 148 | name="Suffix Settings", 149 | default=True 150 | ) 151 | 152 | custom_resolution: IntProperty( 153 | name="Resolution", 154 | default=1024, 155 | min=1, 156 | soft_max=8 * 1024 157 | ) 158 | resolution: EnumProperty( 159 | name="Resolution", 160 | items=( 161 | ('CUSTOM', 'Custom', ''), 162 | ('512', '512', ''), 163 | ('1024', '1024', ''), 164 | ('2048', '2048', ''), 165 | ('4096', '4096', ''), 166 | ), 167 | default='1024' 168 | ) 169 | 170 | margin: IntProperty( 171 | name="Margin", 172 | default=0, 173 | min=0, 174 | max=64 175 | ) 176 | 177 | # removed. now part of bake list 178 | samples: IntProperty( 179 | name="Samples", 180 | default=128, 181 | min=1 182 | ) 183 | individual_samples: BoolProperty( 184 | name="Individual Samples", 185 | default=False, 186 | ) 187 | 188 | use_overwrite: BoolProperty( 189 | name="Overwrite", 190 | description="Be careful with Overwrite! It does what it says!", 191 | default=False 192 | ) 193 | 194 | use_alpha: BoolProperty( 195 | name="Image Alpha", 196 | default=False 197 | ) 198 | 199 | suffix_color: StringProperty( 200 | name="Color", 201 | default="_color", 202 | maxlen=1024, 203 | ) 204 | suffix_metallic: StringProperty( 205 | name="Metallic", 206 | default="_metal", 207 | maxlen=1024, 208 | ) 209 | suffix_roughness: StringProperty( 210 | name="Roughness", 211 | default="_roughness", 212 | maxlen=1024, 213 | ) 214 | suffix_glossiness: StringProperty( 215 | name="Glossiness", 216 | default="_glossiness", 217 | maxlen=1024, 218 | ) 219 | 220 | suffix_specular: StringProperty( 221 | name="Specular ", 222 | default="_specular", 223 | maxlen=1024, 224 | ) 225 | use_invert_roughness: BoolProperty( 226 | name="Glossiness", 227 | description="Glossiness from inverted Roughness.\n\nBakes Roughness texture and inverts the image.", 228 | default=False 229 | ) 230 | 231 | suffix_normal: StringProperty( 232 | name="Normal", 233 | default="_normal", 234 | maxlen=1024, 235 | ) 236 | suffix_bump: StringProperty( 237 | name="Bump (Height)", 238 | default="_bump", 239 | maxlen=1024, 240 | ) 241 | suffix_displacement: StringProperty( 242 | name="Displacement", 243 | default="_displacement", 244 | maxlen=1024, 245 | ) 246 | suffix_vertex_color: StringProperty( 247 | name="Vertex Color", 248 | default="_vertex", 249 | maxlen=1024, 250 | ) 251 | suffix_material_id: StringProperty( 252 | name="Material ID", 253 | default="_MatID", 254 | maxlen=1024, 255 | ) 256 | suffix_diffuse: StringProperty( 257 | name="Diffuse", 258 | default="_diffuse", 259 | maxlen=1024, 260 | ) 261 | suffix_wireframe: StringProperty( 262 | name="Wireframe", 263 | default="_wireframe", 264 | maxlen=1024, 265 | ) 266 | 267 | samples_bump: IntProperty( 268 | name="Bump (Height)", 269 | default=128, 270 | min=1 271 | ) 272 | samples_vertex_color: IntProperty( 273 | name="Vertex Color", 274 | default=128, 275 | min=1 276 | ) 277 | samples_material_id: IntProperty( 278 | name="Material ID", 279 | default=128, 280 | min=1 281 | ) 282 | samples_diffuse: IntProperty( 283 | name="Diffuse", 284 | default=128, 285 | min=1 286 | ) 287 | samples_wireframe: IntProperty( 288 | name="Wireframe", 289 | default=128, 290 | min=1 291 | ) 292 | 293 | color_depth_diffuse: EnumProperty( 294 | name="Diffuse Color Depth", 295 | items=color_depth_items 296 | ) 297 | color_depth_bump: EnumProperty( 298 | name="Bump (Height) Color Depth", 299 | items=color_depth_items 300 | ) 301 | color_depth_vertex_color: EnumProperty( 302 | name="Vertex Color Color Depth", 303 | items=color_depth_items 304 | ) 305 | color_depth_material_id: EnumProperty( 306 | name="Material ID Color Depth", 307 | items=color_depth_items 308 | ) 309 | color_depth_diffuse: EnumProperty( 310 | name="Diffuse Color Depth", 311 | items=color_depth_items 312 | ) 313 | color_depth_wireframe: EnumProperty( 314 | name="Wireframe Color Depth", 315 | items=color_depth_items 316 | ) 317 | 318 | file_path: StringProperty( 319 | name="", 320 | description="directory for textures output", 321 | default="//", 322 | # maxlen=1024, 323 | subtype='DIR_PATH' 324 | ) 325 | 326 | use_texture_folder: BoolProperty( 327 | name="Texture Folder", 328 | description="""Create a texture directory per object named by objects. 329 | \nCombined or Selected to Active: Folder named by active object.\nSingle/Batch: Folder(s) named by object(s)""", 330 | default=False 331 | ) 332 | 333 | use_batch: BoolProperty( 334 | name="Single/Batch", 335 | default=False 336 | ) 337 | 338 | use_selected_to_active: BoolProperty( 339 | name="Selected to Active", 340 | default=False 341 | ) 342 | 343 | bake_mode: EnumProperty( 344 | name="Bake Mode", 345 | items=( 346 | ('COMBINED', 'Combined', 'Bake a single selected object or bake multiple objects with shared UV maps.\n\nThis is like Blenders default bake'), 347 | ('BATCH', 'Single/Batch', 'Bake every selected object separately.'), 348 | ('SELECTED_TO_ACTIVE', 'Selected to Active', ''), 349 | ), 350 | default='COMBINED' 351 | ) 352 | 353 | make_new_material: BoolProperty( 354 | name="Create New Material", 355 | description="Create new materials", 356 | default=False 357 | ) 358 | add_new_material: BoolProperty( 359 | name="Add New Material", 360 | description="Add new material to selected objects.\nIf Selected to Active is active, a new material will be added to active object", 361 | default=False 362 | ) 363 | 364 | new_material_prefix: StringProperty( 365 | name="Material Name", 366 | description="New Material Name. If empty, Material will have name of Object", 367 | default="", 368 | maxlen=1024, 369 | ) 370 | 371 | use_bake_bump: BoolProperty( 372 | name="Bake Bump (Height)", 373 | description="Bake Bump Map from Bump node Height input", 374 | default=False 375 | ) 376 | use_alpha_to_color: BoolProperty( 377 | name="Alpha channel to Color", 378 | description="Add alpha channel to Color Texture", 379 | default=False 380 | ) 381 | use_exclude_transparent_colors: BoolProperty( 382 | name="Exclude Transparent Colors", 383 | description="Exclude colors from nodes with transparency from Color Texture", 384 | default=True 385 | ) 386 | 387 | use_smart_uv_project: BoolProperty( 388 | name="Auto Smart UV Project", 389 | description="", 390 | default=False 391 | ) 392 | 393 | auto_uv_project: EnumProperty( 394 | name="Auto UV Project", 395 | items=( 396 | ('OFF', 'Off', ''), 397 | ('SMART', 'Smart UV Project', ''), 398 | ('LIGHTMAP', 'Lightmap Pack', ''), 399 | ), 400 | default='OFF' 401 | ) 402 | new_uv_map: BoolProperty( 403 | name="New UV Map", 404 | default=False, 405 | description="Add a new UV Map and select.\nIf object has 8 UV maps, no UV map will be added and the selected UV map will not be altered" 406 | ) 407 | 408 | # Smart UV Project: 409 | angle_limit: FloatProperty( 410 | name="Angle Limit", 411 | default=66.0, 412 | min=1.0, 413 | max=89.0 414 | ) 415 | island_margin: FloatProperty( 416 | name="Island Margin", 417 | default=0.0, 418 | min=0.0, 419 | max=1.0 420 | ) 421 | user_area_weight: FloatProperty( 422 | name="Area Weight", 423 | default=0.0, 424 | min=0.0, 425 | max=1.0 426 | ) 427 | use_aspect: BoolProperty( 428 | name="Correct Aspect", 429 | default=True 430 | ) 431 | stretch_to_bounds: BoolProperty( 432 | name="Stretch to UV Bounds", 433 | default=True 434 | ) 435 | 436 | # Lightmap Pack: 437 | share_tex_space: BoolProperty( 438 | name="Share Tex Space", 439 | default=True 440 | ) 441 | new_image: BoolProperty( 442 | name="New Image", 443 | default=False 444 | ) 445 | image_size: IntProperty( 446 | name="Image Size", 447 | default=512, 448 | min=64, 449 | max=5000 450 | ) 451 | pack_quality: IntProperty( 452 | name="Pack Quality", 453 | default=12, 454 | min=1, 455 | max=48 456 | ) 457 | lightmap_margin: FloatProperty( 458 | name="Margin", 459 | default=0.10, 460 | min=0.0, 461 | max=1.0 462 | ) 463 | 464 | use_image_float: BoolProperty( 465 | name="32 bit float", 466 | default=False 467 | ) 468 | 469 | suffix_text_mod: EnumProperty( 470 | name="Convert suffix", 471 | items=( 472 | ('CUSTOM', 'Custom', ''), 473 | ('lower', 'Lower', 'Convert suffix to lowercase letters.'), 474 | ('upper', 'Upper', 'Convert suffix to capital letters.'), 475 | ('title', 'Title', 'Capitalize every word.'), 476 | ), 477 | default='CUSTOM' 478 | ) 479 | 480 | auto_smooth: EnumProperty( 481 | name="Auto Smooth", 482 | items=( 483 | ('OBJECT', 'Object', 'Auto Smooth per Object'), 484 | ('ON', 'ON', 'Bake with Auto Smooth'), 485 | ('OFF', 'OFF', 'Bake without Auto Smooth'), 486 | ), 487 | default='OBJECT' 488 | ) 489 | 490 | use_Bump: BoolProperty(name="Bump (Height)", default=False) 491 | 492 | use_vertex_color: BoolProperty(name="Vertex Color", default=False) 493 | 494 | bake_vertex_colors: EnumProperty( 495 | name="Vertex Colors", 496 | description='Select Vertex Colors to bake', 497 | items=( 498 | ('SELECTED', 'Selected', ''), 499 | ('ACTIVE_RENDER', 'Active Render', ''), 500 | ('ALL', 'All', ''), 501 | ('1', '1', ''), 502 | ('2', '2', ''), 503 | ('3', '3', ''), 504 | ('4', '4', ''), 505 | ('5', '5', ''), 506 | ('6', '6', ''), 507 | ('7', '7', ''), 508 | ('8', '8', ''), 509 | ), 510 | default='SELECTED' 511 | ) 512 | 513 | use_material_id: BoolProperty(name="Material ID", default=False) 514 | 515 | # Diffuse 516 | use_Diffuse: BoolProperty( 517 | name="Diffuse", 518 | description='Does only work in "Combined" and "Single/Batch"', 519 | default=False) 520 | 521 | select_uv_map: EnumProperty( 522 | name="UV Map", 523 | description='Select UV Map to bake on', 524 | items=( 525 | ('SELECTED', 'Selected', ''), 526 | ('ACTIVE_RENDER', 'Active Render', ''), 527 | ('1', '1', ''), 528 | ('2', '2', ''), 529 | ('3', '3', ''), 530 | ('4', '4', ''), 531 | ('5', '5', ''), 532 | ('6', '6', ''), 533 | ('7', '7', ''), 534 | ('8', '8', ''), 535 | ), 536 | default='SELECTED' 537 | ) 538 | 539 | set_selected_uv_map: BoolProperty( 540 | name="Set as selected UV Map", 541 | description='', 542 | default=False) 543 | 544 | select_set_active_render_uv_map: BoolProperty( 545 | name="Set as active render", 546 | description='', 547 | default=False) 548 | 549 | use_shortlist: BoolProperty( 550 | name="Short List", 551 | description='Show the most common Bake Types only', 552 | default=False, 553 | update=reset_bake_list, 554 | ) 555 | 556 | use_wireframe: BoolProperty( 557 | name="Wireframe", 558 | default=False) 559 | 560 | use_pixel_size: BoolProperty( 561 | name="Pixel Size", 562 | default=False) 563 | 564 | wireframe_size: FloatProperty( 565 | name="Size", 566 | default=0.01, 567 | min=0.0, 568 | # max=100.0, 569 | soft_max=100.0 570 | ) 571 | 572 | duplicate_objects: BoolProperty( 573 | name="Duplicate Objects", 574 | description='', 575 | default=False) 576 | 577 | join_duplicate_objects: BoolProperty( 578 | name="Join Duplicate Objects", 579 | description='', 580 | default=False) 581 | 582 | copy_modifiers: BoolProperty( 583 | name="Copy Modifiers", 584 | description='', 585 | default=True) 586 | 587 | duplicate_objects_prefix: StringProperty( 588 | name="Prefix", 589 | default="", 590 | maxlen=1024, 591 | ) 592 | 593 | duplicate_objects_suffix: StringProperty( 594 | name="Suffix", 595 | default="", 596 | maxlen=1024, 597 | ) 598 | 599 | duplicate_object_loc_offset_x: FloatProperty(name="offset x", default=0.0,) 600 | duplicate_object_loc_offset_y: FloatProperty(name="offset y", default=0.0,) 601 | duplicate_object_loc_offset_z: FloatProperty(name="offset z", default=0.0,) 602 | 603 | use_value_differ: BoolProperty( 604 | name="Value Differences", 605 | description='Detect value differences in shader nodes.\n\nThis setting is for Autodetect and for manual detection in the Bake List', 606 | default=True 607 | ) 608 | 609 | use_connected_inputs: BoolProperty( 610 | name="Connected Inputs", 611 | description='Detect connected inputs in shader nodes.\n\nThis setting is for Autodetect and for manual detection in the Bake List', 612 | default=True 613 | ) 614 | -------------------------------------------------------------------------------- /suffixlist.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import StringProperty 3 | from bpy.types import Operator, PropertyGroup, UIList 4 | 5 | DEFAULT_SUFFIXLIST = { 6 | "Color": "_color", 7 | "Metallic": "_metallic", 8 | "Roughness": "_roughness", 9 | 10 | "Normal": "_normal", 11 | # "Bump": "_bump", 12 | "Displacement": "_disp", 13 | 14 | "Alpha": "_alpha", 15 | "Emission": "_emission", 16 | 'Ambient Occlusion': "_ao", 17 | 18 | "Diffuse": "_diffuse", 19 | "Glossiness": "_glossiness", 20 | "Bump": "_bump", 21 | "Vertex Color": "_vertex", 22 | "Material ID": "_MatID", 23 | "Wireframe": "_wireframe", 24 | 25 | "Subsurface": "_subsurface", 26 | "Subsurface Radius": "_subsurface_radius", 27 | "Subsurface Color": "_subsurface_color", 28 | "Specular": "_specular", 29 | "Specular Tint": "_specular_tint", 30 | "Anisotropic": "_anisotropic", 31 | "Anisotropic Rotation": "_anisotropic_rotation", 32 | "Sheen": "_sheen", 33 | "Sheen Tint": "_sheen_tint", 34 | "Clearcoat": "_clearcoat", 35 | "Clearcoat Roughness": "_clearcoat_roughness", 36 | "IOR": "_ior", 37 | "Transmission": "_transmission", 38 | "Transmission Roughness": "_transmission_roughness", 39 | "Clearcoat Normal": "_clearcoat_normal", 40 | "Tangent": "_tangent", 41 | } 42 | 43 | 44 | class PBAKER_SuffixListItem(PropertyGroup): 45 | suffix: StringProperty(name="Suffix") 46 | 47 | 48 | class PBAKER_UL_SuffixList(UIList): 49 | def draw_item(self, context, layout, data, item, icon, active_data, 50 | active_propname, index): 51 | 52 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 53 | layout.label(text=item.name) 54 | layout.prop(item, "suffix", text="") 55 | 56 | 57 | class PBAKER_SUFFIXLIST_OT_Init(Operator): 58 | """Create Suffix List to customize suffixes""" 59 | 60 | bl_idname = "principled_baker_suffixlist.init" 61 | bl_label = "Init Suffixlist" 62 | 63 | def execute(self, context): 64 | suffix_list = context.scene.principled_baker_suffixlist 65 | 66 | if not len(suffix_list): 67 | for jobname, suffix in DEFAULT_SUFFIXLIST.items(): 68 | item = suffix_list.add() 69 | item.name = jobname 70 | item.suffix = suffix 71 | 72 | return{'FINISHED'} 73 | 74 | 75 | class PBAKER_SUFFIXLIST_OT_Delete(Operator): 76 | bl_idname = "principled_baker_suffixlist.delete" 77 | bl_label = "Delete Suffixlist" 78 | 79 | @classmethod 80 | def poll(cls, context): 81 | return context.scene.principled_baker_suffixlist 82 | 83 | def execute(self, context): 84 | context.scene.principled_baker_suffixlist.clear() 85 | return{'FINISHED'} 86 | 87 | 88 | class PBAKER_SUFFIXLIST_OT_Reset(Operator): 89 | """Reset list to default values""" 90 | 91 | bl_idname = "principled_baker_suffixlist.reset" 92 | bl_label = "Update Suffixlist" 93 | 94 | def execute(self, context): 95 | if context.scene.principled_baker_suffixlist: 96 | bpy.ops.principled_baker_suffixlist.delete() 97 | bpy.ops.principled_baker_suffixlist.init() 98 | 99 | return{'FINISHED'} 100 | -------------------------------------------------------------------------------- /uv/project.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def auto_uv_project(objs): 5 | """Auto unwraps UV Map by user settings.""" 6 | 7 | settings = bpy.context.scene.principled_baker_settings 8 | 9 | def smart_project(): 10 | """wrapper for bpy.ops.uv.smart_project() to get all parameters from settings.""" 11 | 12 | bpy.ops.uv.smart_project(angle_limit=settings.angle_limit, 13 | island_margin=settings.island_margin, 14 | user_area_weight=settings.user_area_weight, 15 | use_aspect=settings.use_aspect, 16 | stretch_to_bounds=settings.stretch_to_bounds) 17 | 18 | def lightmap_pack(): 19 | """wrapper for bpy.ops.uv.lightmap_pack() to get all parameters from settings.""" 20 | 21 | bpy.ops.uv.lightmap_pack(PREF_CONTEXT='ALL_FACES', 22 | PREF_PACK_IN_ONE=settings.share_tex_space, 23 | PREF_NEW_UVLAYER=False, # see new UV Map 24 | PREF_APPLY_IMAGE=settings.new_image, 25 | PREF_IMG_PX_SIZE=settings.image_size, 26 | PREF_BOX_DIV=settings.pack_quality, 27 | PREF_MARGIN_DIV=settings.lightmap_margin) 28 | 29 | if settings.auto_uv_project == 'OFF': 30 | return 31 | 32 | if settings.new_uv_map: 33 | for obj in objs: 34 | bpy.context.view_layer.objects.active = obj 35 | bpy.ops.mesh.uv_texture_add() 36 | 37 | for obj in objs: 38 | bpy.context.view_layer.objects.active = obj 39 | if settings.auto_uv_project == 'SMART': 40 | smart_project() 41 | elif settings.auto_uv_project == 'LIGHTMAP': 42 | lightmap_pack() 43 | -------------------------------------------------------------------------------- /uv/select.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def select_uv_map(obj): 5 | """selects a UV Map of given object by user settings. 6 | 7 | Must be restored afer baking! 8 | """ 9 | 10 | settings = bpy.context.scene.principled_baker_settings 11 | 12 | uv_layers = obj.data.uv_layers 13 | 14 | if not settings.select_uv_map == 'SELECTED': 15 | if settings.select_uv_map == 'ACTIVE_RENDER': 16 | for i, uv_layer in enumerate(obj.data.uv_layers): 17 | if uv_layer.active_render: 18 | uv_layers.active_index = i 19 | break 20 | else: 21 | index_uv_layer = int(settings.select_uv_map) - 1 22 | if index_uv_layer <= len(obj.data.uv_layers) - 1: 23 | uv_layers.active_index = index_uv_layer 24 | 25 | if settings.select_set_active_render_uv_map: 26 | if settings.select_uv_map == 'ACTIVE_RENDER': 27 | return 28 | elif settings.select_uv_map == 'SELECTED': 29 | index_uv_layer = uv_layers.active_index 30 | else: 31 | index_uv_layer = int(settings.select_uv_map) - 1 32 | if index_uv_layer <= len(obj.data.uv_layers) - 1: 33 | uv_layers[index_uv_layer].active_render = True 34 | --------------------------------------------------------------------------------